@ant-design/icons#SyncOutlined TypeScript Examples
The following examples show how to use
@ant-design/icons#SyncOutlined.
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: Fee.tsx From subscan-multisig-react with Apache License 2.0 | 6 votes |
export function Fee({ extrinsic }: FeeProps) {
const { t } = useTranslation();
const { fee, calcFee, setFee } = useFee();
useEffect(() => {
if (extrinsic) {
calcFee(extrinsic);
} else {
setFee('Insufficient parameters');
}
}, [calcFee, extrinsic, setFee]);
return <span className="flex items-center h-full">{fee === 'calculating' ? <SyncOutlined spin /> : t(fee)}</span>;
}
Example #2
Source File: index.tsx From fe-v5 with Apache License 2.0 | 5 votes |
function Refresh(props: IProps, ref) {
const [intervalSeconds, setIntervalSeconds] = useState(intervalSecondsCache);
const intervalRef = useRef<NodeJS.Timeout>();
useEffect(() => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
if (intervalSeconds) {
intervalRef.current = setInterval(() => {
props.onRefresh();
}, intervalSeconds * 1000);
}
}, [intervalSeconds]);
useEffect(() => {
return () => {
if (intervalRef.current) {
clearInterval(intervalRef.current);
}
};
}, []);
useImperativeHandle(ref, () => ({
closeRefresh() {
setIntervalSeconds(0);
window.localStorage.setItem('refresh-interval-seconds', '0');
},
}));
return (
<div className='refresh-container'>
<Button className='refresh-btn' icon={<SyncOutlined />} onClick={props.onRefresh} />
<Dropdown
trigger={['click']}
overlay={
<Menu
onClick={(e) => {
setIntervalSeconds(_.toNumber(e.key));
window.localStorage.setItem('refresh-interval-seconds', _.toString(e.key));
}}
>
{_.map(refreshMap, (text, value) => {
return <Menu.Item key={value}>{text}</Menu.Item>;
})}
</Menu>
}
>
<Button>
{refreshMap[intervalSeconds]} <DownOutlined />
</Button>
</Dropdown>
</div>
);
}
Example #3
Source File: index.tsx From nanolooker with MIT License | 5 votes |
RecentTransactions: React.FC = () => {
const { t } = useTranslation();
const { theme, disableLiveTransactions } = React.useContext(
PreferencesContext,
);
const { recentTransactions, isConnected, isError } = useSockets();
return (
<Card
size="small"
title={t("pages.home.recentTransactions")}
extra={<RecentTransactionsPreferences />}
>
<div
className="sticky"
style={{
paddingBottom: "6px",
zIndex: 1,
background: theme === Theme.DARK ? "#1e1e1e" : "#fff",
}}
>
<ConfirmationsPerSecond />
{disableLiveTransactions ? (
<div style={{ textAlign: "center" }}>
{theme === Theme.DARK ? (
<CloseCircleFilled style={{ color: TwoToneColors.SEND_DARK }} />
) : (
<CloseCircleTwoTone twoToneColor={TwoToneColors.SEND} />
)}
<Text style={{ marginLeft: "8px" }} id="live-transactions-disabled">
{t("pages.home.liveUpdatesDisabled")}
</Text>
</div>
) : null}
{isConnected &&
!disableLiveTransactions &&
!recentTransactions.length ? (
<div style={{ textAlign: "center" }}>
<SyncOutlined spin />
<Text style={{ marginLeft: "8px" }}>
{t("pages.home.waitingForTransactions")} ...
</Text>
</div>
) : null}
{!isConnected && !disableLiveTransactions ? (
<div style={{ textAlign: "center" }}>
<SyncOutlined spin />
<Text style={{ marginLeft: "8px" }}>
{isError
? t("pages.home.reconnectingToBlockchain")
: t("pages.home.connectingToBlockchain")}
...
</Text>
</div>
) : null}
</div>
<div
className="gradient-container"
style={{
maxHeight: "1260px",
overflow: "hidden",
}}
>
{recentTransactions.length ? (
<>
<Timeline recentTransactions={recentTransactions} />
<div className="bottom-gradient" />
</>
) : null}
</div>
</Card>
);
}
Example #4
Source File: index.tsx From fe-v5 with Apache License 2.0 | 5 votes |
render() {
const { spinning } = this.state;
const { extraRender, data, showHeader = true } = this.props;
const { title, metric } = data;
const graphConfig = this.getGraphConfig(data);
return (
<div className={this.state.legend ? 'graph-container graph-container-hasLegend' : 'graph-container'}>
{showHeader && (
<div
className='graph-header'
style={{
height: this.headerHeight,
lineHeight: `${this.headerHeight}px`,
}}
>
<div>{title || metric}</div>
<div className='graph-extra'>
<span className='graph-operationbar-item' key='info'>
<Popover placement='left' content={this.getContent()} trigger='click' autoAdjustOverflow={false} getPopupContainer={() => document.body}>
<Button className='' type='link' size='small' onClick={(e) => e.preventDefault()}>
<SettingOutlined />
</Button>
</Popover>
</span>
{this.props.isShowRefresh === false ? null : (
<span className='graph-operationbar-item' key='sync'>
<Button type='link' size='small' onClick={(e) => e.preventDefault()}>
<SyncOutlined onClick={this.refresh} />
</Button>
</span>
)}
{this.props.isShowShare === false ? null : (
<span className='graph-operationbar-item' key='share'>
<Button type='link' size='small' onClick={(e) => e.preventDefault()}>
<ShareAltOutlined onClick={this.shareChart} />
</Button>
</span>
)}
{extraRender && _.isFunction(extraRender) ? extraRender(this) : null}
</div>
</div>
)}
{this.props.graphConfigInnerVisible ? (
<GraphConfigInner
data={graphConfig}
onChange={(...args) => {
this.updateGraphConfig(args[2] || {});
}}
/>
) : null}
{/* 这个spin有点难搞,因为要第一时间获取chart容器的offsetheight */}
{/* <Spin spinning={spinning} wrapperClassName='graph-spin'> */}
{this.renderChart()}
{/* </Spin> */}
<Legend
style={{ display: this.state.legend ? 'block' : 'none', overflowY: 'auto', maxHeight: '35%' }}
graphConfig={graphConfig}
series={this.getZoomedSeries()}
onSelectedChange={this.handleLegendRowSelectedChange}
comparisonOptions={graphConfig.comparisonOptions}
/>
</div>
);
}
Example #5
Source File: index.tsx From metaplex with Apache License 2.0 | 5 votes |
function RunAction({
id,
action,
onFinish,
icon,
}: {
id: string;
action: () => Promise<boolean>;
onFinish?: () => void;
icon: JSX.Element;
}) {
const [state, setRunState] = useState<RunActionState>(
RunActionState.NotRunning,
);
useMemo(() => setRunState(RunActionState.NotRunning), [id]);
const run = async () => {
await setRunState(RunActionState.Running);
const result = await action();
if (result) {
await setRunState(RunActionState.Success);
setTimeout(() => (onFinish ? onFinish() : null), 2000); // Give user a sense of completion before removal from list
} else {
await setRunState(RunActionState.Failed);
}
};
let component;
switch (state) {
case RunActionState.NotRunning:
component = (
<span className="hover-button" onClick={run}>
{icon}
</span>
);
break;
case RunActionState.Failed:
component = (
<span className="hover-button" onClick={run}>
<SyncOutlined />
</span>
);
break;
case RunActionState.Running:
component = <LoadingOutlined />;
break;
case RunActionState.Success:
component = <CheckCircleTwoTone twoToneColor="#52c41a" />;
}
return component;
}
Example #6
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 #7
Source File: UpgradeSection.tsx From posthog-foss with MIT License | 5 votes |
export function UpgradeSection(): JSX.Element {
const { checkForUpdates, toggleSectionOpen } = useActions(pluginsLogic)
const { sectionsOpen } = useValues(pluginsLogic)
const { user } = useValues(userLogic)
const {
filteredPluginsNeedingUpdates,
pluginsNeedingUpdates,
checkingForUpdates,
installedPluginUrls,
updateStatus,
rearranging,
hasUpdatablePlugins,
} = useValues(pluginsLogic)
const upgradeButton = canInstallPlugins(user?.organization) && hasUpdatablePlugins && (
<Button
type="default"
icon={pluginsNeedingUpdates.length > 0 ? <SyncOutlined /> : <CloudDownloadOutlined />}
onClick={(e) => {
e.stopPropagation()
checkForUpdates(true)
}}
loading={checkingForUpdates}
>
{checkingForUpdates
? `Checking plugin ${Object.keys(updateStatus).length + 1} out of ${
Object.keys(installedPluginUrls).length
}`
: pluginsNeedingUpdates.length > 0
? 'Check again for updates'
: 'Check for updates'}
</Button>
)
return (
<>
<div
className="plugins-installed-tab-section-header"
onClick={() => toggleSectionOpen(PluginSection.Upgrade)}
>
<Subtitle
subtitle={
<>
{sectionsOpen.includes(PluginSection.Upgrade) ? (
<CaretDownOutlined />
) : (
<CaretRightOutlined />
)}
{` Plugins to update (${filteredPluginsNeedingUpdates.length})`}
</>
}
buttons={!rearranging && sectionsOpen.includes(PluginSection.Upgrade) && upgradeButton}
/>
</div>
{sectionsOpen.includes(PluginSection.Upgrade) ? (
<>
{pluginsNeedingUpdates.length > 0 ? (
<Row gutter={16} style={{ marginTop: 16 }}>
{filteredPluginsNeedingUpdates.length > 0 ? (
<>
{filteredPluginsNeedingUpdates.map((plugin) => (
<InstalledPlugin key={plugin.id} plugin={plugin} showUpdateButton />
))}
</>
) : (
<p style={{ margin: 10 }}>No plugins match your search.</p>
)}
</Row>
) : (
<p style={{ margin: 10 }}>All your plugins are up to date. Great work!</p>
)}
</>
) : null}
</>
)
}
Example #8
Source File: Metrics.tsx From fe-v5 with Apache License 2.0 | 4 votes |
export default function Metrics(props: IProps) {
const { range, setRange, match } = props;
const [refreshFlag, setRefreshFlag] = useState(_.uniqueId('refreshFlag_'));
const [search, setSearch] = useState('');
const [metrics, setMetrics] = useState([]);
const [metricsDesc, setMetricsDesc] = useState({});
const [activeKey, setActiveKey] = useState('all');
const [metricPrefixes, setMetricPrefixes] = useState([]);
const [selectedMetrics, setSelectedMetrics] = useState([]);
const [step, setStep] = useState<number>();
const matchStr = getMatchStr(match);
const renderMetricList = (metrics = [], metricTabKey: string) => {
const filtered = _.filter(metrics, (metric) => {
let flag = true;
flag = metricTabKey === 'all' ? true : metric.indexOf(metricTabKey) === 0;
if (flag && search) {
try {
const reg = new RegExp(search, 'gi');
flag = reg.test(metric);
} catch (e) {
flag = false;
}
}
return flag;
});
return (
<div className='tabPane' style={{ height: 240, overflow: 'auto' }}>
{filtered.length ? (
<ul className='n9e-metric-views-metrics-content' style={{ border: 'none' }}>
{_.map(filtered, (metric, i) => {
return (
<li
className='item'
key={i}
onClick={() => {
setSelectedMetrics(_.union(_.concat(metric, selectedMetrics)));
}}
>
<span>{metric}</span>
{_.find(selectedMetrics, (sm) => sm === metric) ? <span style={{ marginLeft: 8 }}>+1</span> : null}
{metricsDesc[metric] ? (
<Tooltip title={metricsDesc[metric]}>
<span className='desc'>{metricsDesc[metric]}</span>
</Tooltip>
) : null}
</li>
);
})}
</ul>
) : (
<div style={{ textAlign: 'center' }}>No data</div>
)}
</div>
);
};
useEffect(() => {
if (matchStr) {
getMetricValues(matchStr, range).then((res) => {
const _metrics = _.union(res);
const metricPrefixes = _.union(
_.compact(
_.map(_metrics, (m) => {
return _.get(_.split(m, '_'), '[0]');
}),
),
);
setMetrics(_metrics);
setMetricPrefixes(metricPrefixes);
getMetricsDesc(_metrics).then((res) => {
setMetricsDesc(res);
});
});
}
}, [refreshFlag, matchStr]);
useEffect(() => {
setSelectedMetrics([]);
setActiveKey('all');
setMetrics([]);
}, [match.id, matchStr]);
return (
<div className='n9e-metric-views-metrics'>
<div>
<div className='n9e-metric-views-metrics-header'>
<div className='metric-page-title'>监控指标</div>
<Input
prefix={<SearchOutlined />}
value={search}
onChange={(e) => {
setSearch(e.target.value);
}}
placeholder='搜索,空格分隔多个关键字'
addonAfter={
<SyncOutlined
style={{ cursor: 'pointer' }}
onClick={() => {
setRefreshFlag(_.uniqueId('refreshFlag_'));
}}
/>
}
/>
</div>
<div>
{metrics.length > 0 ? (
<>
<Card
size='small'
style={{ width: '100%' }}
tabList={_.map(['all', ...metricPrefixes], (item) => {
return {
key: item,
tab: item,
};
})}
activeTabKey={activeKey}
onTabChange={setActiveKey}
>
<div>{renderMetricList(metrics, activeKey)}</div>
</Card>
<Row style={{ padding: '10px 0' }}>
<Col span={8}>
<Space>
<DateRangePicker
value={range}
onChange={(e) => {
setRange(e);
}}
/>
<Resolution
onChange={(v) => {
setStep(v === null ? undefined : v);
}}
initialValue={step}
/>
<Button
style={{ padding: '4px 8px' }}
onClick={() => {
setRange({
...range,
refreshFlag: _.uniqueId('refreshFlag_'),
});
}}
icon={<SyncOutlined />}
></Button>
</Space>
</Col>
<Col span={16} style={{ textAlign: 'right' }}>
<Button
onClick={() => {
setSelectedMetrics([]);
}}
disabled={!selectedMetrics.length}
style={{ background: '#fff' }}
>
清空图表
</Button>
</Col>
</Row>
{_.map(selectedMetrics, (metric, i) => {
return (
<Graph
key={metric}
metric={metric}
match={match}
range={range}
step={step}
onClose={() => {
const newselectedMetrics = [...selectedMetrics];
newselectedMetrics.splice(i, 1);
setSelectedMetrics(newselectedMetrics);
}}
/>
);
})}
</>
) : (
<div style={{ marginTop: 12 }}>暂无指标数据,请选择左侧 Lables</div>
)}
</div>
</div>
</div>
);
}
Example #9
Source File: palette.tsx From jmix-frontend with Apache License 2.0 | 4 votes |
palette = () =>
<Palette>
<Category name="Text">
<Component name="Formatted Message">
<Variant>
<FormattedMessage />
</Variant>
</Component>
<Component name="Heading">
<Variant name='h1'>
<Typography.Title></Typography.Title>
</Variant>
<Variant name='h2'>
<Typography.Title level = {2}></Typography.Title>
</Variant>
<Variant name='h3'>
<Typography.Title level = {3}></Typography.Title>
</Variant>
<Variant name='h4'>
<Typography.Title level = {4}></Typography.Title>
</Variant>
<Variant name='h5'>
<Typography.Title level = {5}></Typography.Title>
</Variant>
</Component>
<Component name='Text'>
<Variant>
<Typography.Text></Typography.Text>
</Variant>
<Variant name = 'Secondary'>
<Typography.Text type="secondary"></Typography.Text>
</Variant>
<Variant name = 'Success'>
<Typography.Text type="success"></Typography.Text>
</Variant>
<Variant name = 'Warning'>
<Typography.Text type="warning"></Typography.Text>
</Variant>
<Variant name = 'Danger'>
<Typography.Text type="danger"></Typography.Text>
</Variant>
<Variant name = 'Disabled'>
<Typography.Text disabled></Typography.Text>
</Variant>
</Component>
</Category>
<Category name="Layout">
<Component name="Divider">
<Variant>
<Divider />
</Variant>
</Component>
<Component name="Grid">
<Variant name="Simple Row">
<Row></Row>
</Variant>
<Variant name="Two columns">
<Row>
<Col span={12}></Col>
<Col span={12}></Col>
</Row>
</Variant>
<Variant name="Three columns">
<Row>
<Col span={8}></Col>
<Col span={8}></Col>
<Col span={8}></Col>
</Row>
</Variant>
</Component>
<Component name="Space">
<Variant>
<Space />
</Variant>
<Variant name="Small">
<Space size={"small"} />
</Variant>
<Variant name="Large">
<Space size={"large"} />
</Variant>
</Component>
</Category>
<Category name="Controls">
<Component name="Autocomplete">
<Variant>
<AutoComplete placeholder="input here" />
</Variant>
</Component>
<Component name="Button">
<Variant>
<Button></Button>
</Variant>
<Variant name="Primary">
<Button type="primary" ></Button>
</Variant>
<Variant name="Link">
<Button type="link" ></Button>
</Variant>
<Variant name="Dropdown">
<Dropdown
trigger={['click']}
overlay={<Menu>
<Menu.Item>
</Menu.Item>
<Menu.Item>
</Menu.Item>
<Menu.Item>
</Menu.Item>
</Menu>}
>
<Button></Button>
</Dropdown>
</Variant>
</Component>
<Component name="Checkbox">
<Variant>
<Checkbox />
</Variant>
</Component>
<Component name='Switch'>
<Variant>
<Switch />
</Variant>
</Component>
<Component name='Radio Group'>
<Variant>
<Radio.Group>
<Radio value={1}>A</Radio>
<Radio value={2}>B</Radio>
<Radio value={3}>C</Radio>
<Radio value={4}>D</Radio>
</Radio.Group>
</Variant>
<Variant name = 'Button'>
<Radio.Group>
<Radio.Button value={1}>A</Radio.Button>
<Radio.Button value={2}>B</Radio.Button>
<Radio.Button value={3}>C</Radio.Button>
<Radio.Button value={4}>D</Radio.Button>
</Radio.Group>
</Variant>
</Component>
<Component name="DatePicker">
<Variant>
<DatePicker />
</Variant>
<Variant name="Range">
<DatePicker.RangePicker />
</Variant>
</Component>
<Component name="TimePicker">
<Variant>
<TimePicker />
</Variant>
<Variant name="Range">
<TimePicker.RangePicker />
</Variant>
</Component>
<Component name="Input">
<Variant>
<Input />
</Variant>
<Variant name='Number'>
<InputNumber />
</Variant>
</Component>
<Component name='Select'>
<Variant>
<Select defaultValue="1">
<Select.Option value="1">1</Select.Option>
<Select.Option value="2">2</Select.Option>
</Select>
</Variant>
<Variant name='Multiple'>
<Select
defaultValue={["1"]}
mode="multiple"
allowClear
>
<Select.Option value="1">1</Select.Option>
<Select.Option value="2">2</Select.Option>
</Select>
</Variant>
</Component>
<Component name="Link">
<Variant>
<Typography.Link href="" target="_blank">
</Typography.Link>
</Variant>
</Component>
<Component name='Slider'>
<Variant>
<Slider defaultValue={30} />
</Variant>
<Variant name = 'Range'>
<Slider range defaultValue={[20, 50]}/>
</Variant>
</Component>
</Category>
<Category name="Data Display">
<Component name="Field">
<Variant>
<Field
entityName={ENTITY_NAME}
disabled={readOnlyMode}
propertyName=''
formItemProps={{
style: { marginBottom: "12px" }
}}
/>
</Variant>
</Component>
<Component name="Card">
<Variant>
<Card />
</Variant>
<Variant name="With Title">
<Card>
<Card title="Card title">
<p>Card content</p>
</Card>
</Card>
</Variant>
<Variant name="My custom card">
<Card>
<Card title="Card title">
<p>Card content</p>
<Avatar />
</Card>
</Card>
</Variant>
</Component>
<Component name="Tabs">
<Variant>
<Tabs defaultActiveKey="1">
<Tabs.TabPane tab="Tab 1" key="1">
Content of Tab Pane 1
</Tabs.TabPane>
<Tabs.TabPane tab="Tab 2" key="2">
Content of Tab Pane 2
</Tabs.TabPane>
<Tabs.TabPane tab="Tab 3" key="3">
Content of Tab Pane 3
</Tabs.TabPane>
</Tabs>
</Variant>
<Variant name = "Tab Pane">
<Tabs.TabPane>
</Tabs.TabPane>
</Variant>
</Component>
<Component name="Collapse">
<Variant>
<Collapse defaultActiveKey='1'>
<Collapse.Panel header="This is panel header 1" key="1">
</Collapse.Panel>
<Collapse.Panel header="This is panel header 2" key="2">
</Collapse.Panel>
<Collapse.Panel header="This is panel header 3" key="3">
</Collapse.Panel>
</Collapse>
</Variant>
</Component>
<Component name="Image">
<Variant>
<Image
width={200}
src=""
/>
</Variant>
</Component>
<Component name="Avatar">
<Variant>
<Avatar icon={<UserOutlined />} />
</Variant>
<Variant name="Image">
<Avatar src="https://joeschmoe.io/api/v1/random" />
</Variant>
</Component>
<Component name="Badge">
<Variant>
<Badge count={1}>
</Badge>
</Variant>
</Component>
<Component name="Statistic">
<Variant>
<Statistic title="Title" value={112893} />
</Variant>
</Component>
<Component name="Alert">
<Variant name="Success">
<Alert message="Text" type="success" />
</Variant>
<Variant name="Info">
<Alert message="Text" type="info" />
</Variant>
<Variant name="Warning">
<Alert message="Text" type="warning" />
</Variant>
<Variant name="Error">
<Alert message="Text" type="error" />
</Variant>
</Component>
<Component name='List'>
<Variant>
<List
bordered
dataSource={[]}
renderItem={item => (
<List.Item>
</List.Item>
)}
/>
</Variant>
</Component>
</Category>
<Category name="Icons">
<Component name="Arrow">
<Variant name = 'Up'>
<ArrowUpOutlined />
</Variant>
<Variant name = 'Down'>
<ArrowDownOutlined />
</Variant>
<Variant name = 'Left'>
<ArrowLeftOutlined />
</Variant>
<Variant name = 'Right'>
<ArrowRightOutlined />
</Variant>
</Component>
<Component name = 'Question'>
<Variant>
<QuestionOutlined />
</Variant>
<Variant name = 'Circle'>
<QuestionCircleOutlined />
</Variant>
</Component>
<Component name = 'Plus'>
<Variant>
<PlusOutlined />
</Variant>
<Variant name = 'Circle'>
<PlusCircleOutlined />
</Variant>
</Component>
<Component name = 'Info'>
<Variant>
<InfoOutlined />
</Variant>
<Variant name = 'Circle'>
<InfoCircleOutlined />
</Variant>
</Component>
<Component name = 'Exclamation'>
<Variant>
<ExclamationOutlined />
</Variant>
<Variant name = 'Circle'>
<ExclamationCircleOutlined />
</Variant>
</Component>
<Component name = 'Close'>
<Variant>
<CloseOutlined />
</Variant>
<Variant name = 'Circle'>
<CloseCircleOutlined />
</Variant>
</Component>
<Component name = 'Check'>
<Variant>
<CheckOutlined />
</Variant>
<Variant name = 'Circle'>
<CheckCircleOutlined />
</Variant>
</Component>
<Component name = 'Edit'>
<Variant>
<EditOutlined />
</Variant>
</Component>
<Component name = 'Copy'>
<Variant>
<CopyOutlined />
</Variant>
</Component>
<Component name = 'Delete'>
<Variant>
<DeleteOutlined />
</Variant>
</Component>
<Component name = 'Bars'>
<Variant>
<BarsOutlined />
</Variant>
</Component>
<Component name = 'Bell'>
<Variant>
<BellOutlined />
</Variant>
</Component>
<Component name = 'Clear'>
<Variant>
<ClearOutlined />
</Variant>
</Component>
<Component name = 'Download'>
<Variant>
<DownloadOutlined />
</Variant>
</Component>
<Component name = 'Upload'>
<Variant>
<UploadOutlined />
</Variant>
</Component>
<Component name = 'Sync'>
<Variant>
<SyncOutlined />
</Variant>
</Component>
<Component name = 'Save'>
<Variant>
<SaveOutlined />
</Variant>
</Component>
<Component name = 'Search'>
<Variant>
<SearchOutlined />
</Variant>
</Component>
<Component name = 'Settings'>
<Variant>
<SettingOutlined />
</Variant>
</Component>
<Component name = 'Paperclip'>
<Variant>
<PaperClipOutlined />
</Variant>
</Component>
<Component name = 'Phone'>
<Variant>
<PhoneOutlined />
</Variant>
</Component>
<Component name = 'Mail'>
<Variant>
<MailOutlined />
</Variant>
</Component>
<Component name = 'Home'>
<Variant>
<HomeOutlined />
</Variant>
</Component>
<Component name = 'Contacts'>
<Variant>
<ContactsOutlined />
</Variant>
</Component>
<Component name = 'User'>
<Variant>
<UserOutlined />
</Variant>
<Variant name = 'Add'>
<UserAddOutlined />
</Variant>
<Variant name = 'Remove'>
<UserDeleteOutlined />
</Variant>
</Component>
<Component name = 'Team'>
<Variant>
<TeamOutlined />
</Variant>
</Component>
</Category>
</Palette>
Example #10
Source File: WidgetActionDropdown.tsx From datart with Apache License 2.0 | 4 votes |
WidgetActionDropdown: React.FC<WidgetActionDropdownProps> = memo(
({ widget }) => {
const { editing: boardEditing } = useContext(BoardContext);
const widgetAction = useWidgetAction();
const dataChart = useContext(WidgetChartContext)!;
const t = useI18NPrefix(`viz.widget.action`);
const menuClick = useCallback(
({ key }) => {
widgetAction(key, widget);
},
[widgetAction, widget],
);
const getAllList = useCallback(() => {
const allWidgetActionList: WidgetActionListItem<widgetActionType>[] = [
{
key: 'refresh',
label: t('refresh'),
icon: <SyncOutlined />,
},
{
key: 'fullScreen',
label: t('fullScreen'),
icon: <FullscreenOutlined />,
},
{
key: 'edit',
label: t('edit'),
icon: <EditOutlined />,
},
{
key: 'delete',
label: t('delete'),
icon: <DeleteOutlined />,
danger: true,
},
{
key: 'info',
label: t('info'),
icon: <InfoOutlined />,
},
{
key: 'lock',
label: t('lock'),
icon: <LockOutlined />,
},
{
key: 'makeLinkage',
label: t('makeLinkage'),
icon: <LinkOutlined />,
divider: true,
},
{
key: 'closeLinkage',
label: t('closeLinkage'),
icon: <CloseCircleOutlined />,
danger: true,
},
{
key: 'makeJump',
label: t('makeJump'),
icon: <BranchesOutlined />,
divider: true,
},
{
key: 'closeJump',
label: t('closeJump'),
icon: <CloseCircleOutlined />,
danger: true,
},
];
return allWidgetActionList;
}, [t]);
const actionList = useMemo(() => {
return (
getWidgetActionList({
allList: getAllList(),
widget,
boardEditing,
chartGraphId: dataChart?.config?.chartGraphId,
}) || []
);
}, [boardEditing, dataChart?.config?.chartGraphId, getAllList, widget]);
const dropdownList = useMemo(() => {
const menuItems = actionList.map(item => {
return (
<React.Fragment key={item.key}>
{item.divider && <Menu.Divider />}
<Menu.Item
danger={item.danger}
icon={item.icon}
disabled={item.disabled}
key={item.key}
>
{item.label}
</Menu.Item>
</React.Fragment>
);
});
return <Menu onClick={menuClick}>{menuItems}</Menu>;
}, [actionList, menuClick]);
if (actionList.length === 0) {
return null;
}
return (
<Dropdown
className="widget-tool-dropdown"
overlay={dropdownList}
placement="bottomCenter"
trigger={['click']}
arrow
>
<Button icon={<EllipsisOutlined />} type="link" />
</Dropdown>
);
},
)
Example #11
Source File: index.tsx From posthog-foss with MIT License | 4 votes |
export function ObjectTags({
tags,
onTagSave, // Required unless `staticOnly`
onTagDelete, // Required unless `staticOnly`
saving, // Required unless `staticOnly`
tagsAvailable,
style = {},
staticOnly = false,
id, // For pages that allow multiple object tags
}: ObjectTagsProps): JSX.Element {
const [addingNewTag, setAddingNewTag] = useState(false)
const [newTag, setNewTag] = useState('')
const [deletedTags, setDeletedTags] = useState<string[]>([]) // we use this state var to remove items immediately from UI while API requests are processed
const handleDelete = (tag: string, currentTags?: string[], propertyId?: string): void => {
setDeletedTags([...deletedTags, tag])
onTagDelete && onTagDelete(tag, currentTags, propertyId)
}
useEffect(() => {
if (!saving) {
setAddingNewTag(false)
setNewTag('')
}
}, [saving])
/** Displaying nothing is confusing, so in case of empty static tags we use a dash as a placeholder */
const showPlaceholder = staticOnly && !tags.length
if (showPlaceholder && !style.color) {
style.color = 'var(--muted)'
}
return (
<div style={style}>
{showPlaceholder
? '—'
: tags
.filter((t) => !!t)
.map((tag, index) => {
return (
<Tag
key={index}
color={COLOR_OVERRIDES[tag] || colorForString(tag)}
style={{ marginTop: 8 }}
>
{tag}{' '}
{!staticOnly &&
onTagDelete &&
(deletedTags.includes(tag) ? (
<SyncOutlined spin />
) : (
<CloseOutlined
style={{ cursor: 'pointer' }}
onClick={() => handleDelete(tag, tags, id)}
/>
))}
</Tag>
)
})}
{!staticOnly && onTagSave && saving !== undefined && (
<span style={{ display: 'inline-flex', fontWeight: 400 }}>
<Tag
onClick={() => setAddingNewTag(true)}
data-attr="button-add-tag"
style={{
cursor: 'pointer',
borderStyle: 'dashed',
backgroundColor: '#ffffff',
display: addingNewTag ? 'none' : 'initial',
}}
>
<PlusOutlined /> New Tag
</Tag>
{addingNewTag && (
<SelectGradientOverflow
size="small"
onBlur={() => setAddingNewTag(false)}
data-attr="new-tag-input"
autoFocus
allowClear
autoClearSearchValue
defaultOpen
showSearch
style={{ width: 160 }}
onChange={(value) => {
onTagSave(value, tags, id)
setNewTag('')
setAddingNewTag(false)
}}
disabled={saving}
loading={saving}
onSearch={(newInput) => {
setNewTag(newInput)
}}
placeholder='try "official"'
>
{newTag ? (
<Select.Option
key={`${newTag}_${id}`}
value={newTag}
className="ph-no-capture"
data-attr="new-tag-option"
>
New Tag: {newTag}
</Select.Option>
) : (
(!tagsAvailable || !tagsAvailable.length) && (
<Select.Option key="__" value="__" disabled style={{ color: 'var(--muted)' }}>
Type to add a new tag
</Select.Option>
)
)}
{tagsAvailable &&
tagsAvailable.map((tag) => (
<Select.Option key={tag} value={tag} className="ph-no-capture">
{tag}
</Select.Option>
))}
</SelectGradientOverflow>
)}
</span>
)}
</div>
)
}
Example #12
Source File: index.tsx From nanolooker with MIT License | 4 votes |
BlockDetails: React.FC = () => {
const { t } = useTranslation();
const { theme, fiat } = React.useContext(PreferencesContext);
const {
marketStatistics: {
currentPrice,
priceStats: { bitcoin: { [fiat]: btcCurrentPrice = 0 } } = {
bitcoin: { [fiat]: 0 },
},
},
isInitialLoading: isMarketStatisticsInitialLoading,
} = React.useContext(MarketStatisticsContext);
const {
blocks,
blocksInfo,
isLoading: isBlocksInfoLoading,
} = React.useContext(BlocksInfoContext);
const { knownAccounts } = React.useContext(KnownAccountsContext);
const isSmallAndLower = !useMediaQuery("(min-width: 576px)");
const skeletonProps = {
active: true,
paragraph: false,
loading: isBlocksInfoLoading,
};
const blockInfo = blocksInfo?.blocks?.[blocks[0]];
const {
subtype,
block_account: blockAccount,
source_account: sourceAccount,
height,
contents: {
type = "",
representative = "",
link = "",
link_as_account: linkAsAccount = "",
previous = "",
signature = "",
work = "",
} = {},
successor,
} = blockInfo || {};
const modifiedTimestamp = Number(blockInfo?.local_timestamp) * 1000;
const amount = new BigNumber(rawToRai(blockInfo?.amount || 0)).toNumber();
const fiatAmount = new BigNumber(amount)
.times(currentPrice)
.toFormat(CurrencyDecimal?.[fiat]);
const btcAmount = new BigNumber(amount)
.times(currentPrice)
.dividedBy(btcCurrentPrice)
.toFormat(12);
const balance = new BigNumber(rawToRai(blockInfo?.balance || 0)).toNumber();
const fiatBalance = new BigNumber(balance)
.times(currentPrice)
.toFormat(CurrencyDecimal?.[fiat]);
const btcBalance = new BigNumber(balance)
.times(currentPrice)
.dividedBy(btcCurrentPrice)
.toFormat(12);
let linkAccountLabel = "";
if (subtype === "send") {
linkAccountLabel = t("pages.block.receiver");
} else if (subtype === "receive") {
linkAccountLabel = t("pages.block.sender");
}
const secondAccount = isValidAccountAddress(sourceAccount || "")
? sourceAccount
: linkAsAccount;
const blockAccountAlias = knownAccounts.find(
({ account: knownAccount }) => knownAccount === blockAccount,
)?.alias;
const secondAccountAlias = knownAccounts.find(
({ account: knownAccount }) => knownAccount === secondAccount,
)?.alias;
const representativeAlias = knownAccounts.find(
({ account: knownAccount }) => knownAccount === representative,
)?.alias;
const isConfirmed = toBoolean(blockInfo?.confirmed);
return (
<>
{!isBlocksInfoLoading && !blockInfo ? (
<Card bordered={false}>
<Title level={3}>{t("pages.block.blockNotFound")}</Title>
<Text>{t("pages.block.blockNotFoundInfo")}</Text>
</Card>
) : null}
{isBlocksInfoLoading || blockInfo ? (
<>
<Card
size="small"
bordered={false}
className="detail-layout"
style={{ marginBottom: "12px" }}
>
<Row gutter={6}>
<Col xs={24}>
<BlockHeader />
</Col>
</Row>
<Row gutter={6}>
{isSmallAndLower ? null : (
<Col xs={24} sm={6} xl={4}>
{t("pages.block.blockSubtype")}
</Col>
)}
<Col xs={24} sm={18} xl={20}>
<Skeleton
{...skeletonProps}
title={{ width: isSmallAndLower ? "50%" : "20%" }}
>
<Tooltip
placement={isSmallAndLower ? "right" : "top"}
title={t(
`pages.block.${
isConfirmed ? "confirmed" : "pending"
}Status`,
)}
>
<Tag
icon={
isConfirmed ? (
<CheckCircleOutlined />
) : (
<SyncOutlined spin />
)
}
color={
// @ts-ignore
TwoToneColors[
`${(subtype || type).toUpperCase()}${
theme === Theme.DARK ? "_DARK" : ""
}`
]
}
className={`tag-${subtype || type}`}
>
{t(`transaction.${subtype || type}`)}
</Tag>
</Tooltip>
</Skeleton>
</Col>
</Row>
<Row gutter={6}>
<Col xs={24} sm={6} xl={4}>
{t("common.account")}
</Col>
<Col xs={24} sm={18} xl={20}>
<Skeleton {...skeletonProps}>
{blockAccountAlias ? (
<strong style={{ display: "block" }}>
{blockAccountAlias}
</strong>
) : null}
<Link to={`/account/${blockAccount}`} className="break-word">
{blockAccount}
</Link>
</Skeleton>
</Col>
</Row>
<Row gutter={6}>
<Col xs={24} sm={6} xl={4}>
{t("transaction.amount")}
</Col>
<Col xs={24} sm={18} xl={20}>
<LoadingStatistic
isLoading={skeletonProps.loading}
prefix="Ӿ"
value={
amount >= 1 ? amount : new BigNumber(amount).toFormat()
}
/>
<Skeleton
{...skeletonProps}
loading={
skeletonProps.loading || isMarketStatisticsInitialLoading
}
title={{ width: isSmallAndLower ? "100%" : "33%" }}
>
{`${CurrencySymbol?.[fiat]} ${fiatAmount} / ${btcAmount} BTC`}
</Skeleton>
</Col>
</Row>
<Row gutter={6}>
<Col xs={24} sm={6} xl={4}>
{t("common.balance")}
</Col>
<Col xs={24} sm={18} xl={20}>
<Skeleton
{...skeletonProps}
title={{ width: isSmallAndLower ? "100%" : "33%" }}
>
Ӿ {new BigNumber(balance).toFormat()}
<br />
</Skeleton>
<Skeleton
{...skeletonProps}
loading={
skeletonProps.loading || isMarketStatisticsInitialLoading
}
title={{ width: isSmallAndLower ? "100%" : "33%" }}
>
{`${CurrencySymbol?.[fiat]} ${fiatBalance} / ${btcBalance} BTC`}
</Skeleton>
</Col>
</Row>
{linkAccountLabel ? (
<Row gutter={6}>
<Col xs={24} sm={6} xl={4}>
{linkAccountLabel}
</Col>
<Col xs={24} sm={18} xl={20}>
{secondAccountAlias ? (
<strong
style={{
display: "block",
}}
>
{secondAccountAlias}
</strong>
) : null}
<Link to={`/account/${secondAccount}`} className="break-word">
{secondAccount}
</Link>
</Col>
</Row>
) : null}
{representative ? (
<Row gutter={6}>
<Col xs={24} sm={6} xl={4}>
{t("common.representative")}
</Col>
<Col xs={24} sm={18} xl={20}>
{representativeAlias ? (
<strong
style={{
display: "block",
}}
>
{representativeAlias}
</strong>
) : null}
<Link
to={`/account/${representative}`}
className="break-word"
>
{representative}
</Link>
</Col>
</Row>
) : null}
<Row gutter={6}>
<Col xs={24} sm={6} xl={4}>
{t("pages.block.height")}
</Col>
<Col xs={24} sm={18} xl={20}>
<Skeleton {...skeletonProps}>{height}</Skeleton>
</Col>
</Row>
{modifiedTimestamp ? (
<Row gutter={6}>
<Col xs={24} sm={6} xl={4}>
{t("common.date")}
</Col>
<Col xs={24} sm={18} xl={20}>
{timestampToDate(modifiedTimestamp)}{" "}
<span className="color-muted" style={{ fontSize: "12px" }}>
(
<TimeAgo
locale={i18next.language}
datetime={modifiedTimestamp}
live={false}
/>
)
</span>
</Col>
</Row>
) : null}
<Row gutter={6}>
<Col xs={24} sm={6} xl={4}>
{t("pages.block.previousBlock")}
</Col>
<Col xs={24} sm={18} xl={20}>
<Skeleton
{...skeletonProps}
title={{ width: isSmallAndLower ? "100%" : "50%" }}
>
{isValidBlockHash(previous) ? (
<Link to={`/block/${previous}`} className="break-word">
{previous}
</Link>
) : null}
{isNullAccountBlockHash(previous) ? (
<Text>{t("pages.block.openAccountBlock")}</Text>
) : null}
</Skeleton>
</Col>
</Row>
<Row gutter={6}>
<Col xs={24} sm={6} xl={4}>
{t("pages.block.successorBlock")}
</Col>
<Skeleton
{...skeletonProps}
title={{ width: isSmallAndLower ? "100%" : "50%" }}
></Skeleton>
<Col xs={24} sm={18} xl={20}>
{isValidBlockHash(successor) ? (
<Link to={`/block/${successor}`} className="break-word">
{successor}
</Link>
) : null}
{isNullAccountBlockHash(successor) ? (
<Text>{t("pages.block.lastAccountBlock")}</Text>
) : null}
</Col>
</Row>
{link && subtype === "receive" ? (
<Row gutter={6}>
<Col xs={24} sm={6} xl={4}>
{t("pages.block.matchingSendBlock")}
</Col>
<Skeleton
{...skeletonProps}
title={{ width: isSmallAndLower ? "100%" : "50%" }}
></Skeleton>
<Col xs={24} sm={18} xl={20}>
{isValidBlockHash(link) ? (
<Link to={`/block/${link}`} className="break-word">
{link}
</Link>
) : (
t("pages.block.noMatchingSendBlock")
)}
</Col>
</Row>
) : null}
<Row gutter={6}>
<Col xs={24} sm={6} xl={4}>
{t("pages.block.signature")}
</Col>
<Col xs={24} sm={18} xl={20}>
<Skeleton {...skeletonProps}>
<span className="break-word">{signature}</span>
</Skeleton>
</Col>
</Row>
<Row gutter={6}>
<Col xs={24} sm={6} xl={4}>
{t("pages.block.work")}
</Col>
<Col xs={24} sm={18} xl={20}>
<Skeleton
{...skeletonProps}
title={{ width: isSmallAndLower ? "100%" : "33%" }}
>
{work}
</Skeleton>
</Col>
</Row>
</Card>
<Title level={3}>{t("pages.block.originalBlockContent")}</Title>
<Card size="small">
<Skeleton {...skeletonProps} paragraph>
<pre style={{ fontSize: "12px", marginBottom: 0 }}>
{JSON.stringify(blockInfo, null, 2)}
</pre>
</Skeleton>
</Card>
</>
) : null}
</>
);
}
Example #13
Source File: index.tsx From nanolooker with MIT License | 4 votes |
TransactionsTable = ({
scrollTo,
data,
isLoading,
showPaginate,
isPaginated,
pageSize,
currentPage,
totalPages,
setCurrentPage,
setCurrentHead,
}: TransactionsTableProps) => {
const { t } = useTranslation();
const { theme, natricons } = React.useContext(PreferencesContext);
const { knownAccounts } = React.useContext(KnownAccountsContext);
const isLargeAndHigher = useMediaQuery("(min-width: 992px)");
const smallNatriconSize = !useMediaQuery("(min-width: 768px)");
return (
<Card size="small" className="transaction-card" id={scrollTo}>
{isLoading ? (
<div className="ant-spin-nested-loading">
<div>
<div className="ant-spin ant-spin-spinning">
<span className="ant-spin-dot ant-spin-dot-spin">
<i className="ant-spin-dot-item"></i>
<i className="ant-spin-dot-item"></i>
<i className="ant-spin-dot-item"></i>
<i className="ant-spin-dot-item"></i>
</span>
</div>
</div>
</div>
) : null}
{isLargeAndHigher ? (
<Row
gutter={[{ xs: 6, sm: 12, md: 12, lg: 12 }, 12]}
className="row-header color-muted"
>
<Col xs={0} lg={2}>
{t("transaction.type")}
</Col>
{natricons ? <Col xs={0} lg={2}></Col> : null}
<Col xs={0} lg={natricons ? 12 : 14}>
{t("transaction.accountAndBlock")}
</Col>
<Col xs={0} lg={5}>
{t("transaction.amount")}
</Col>
<Col xs={0} lg={3} style={{ textAlign: "right" }}>
{t("common.date")}
</Col>
</Row>
) : null}
{data?.length ? (
<>
{data.map(
(
{
subtype,
type,
account: historyAccount,
amount,
representative,
hash,
confirmed,
local_timestamp: localTimestamp,
}: History,
index: number,
) => {
const transactionType = subtype || type;
const themeColor = `${transactionType.toUpperCase()}${
theme === Theme.DARK ? "_DARK" : ""
}`;
// When transaction is a representative change, the account is the representative
const account =
transactionType === "change" ? representative : historyAccount;
const knownAccount =
account &&
knownAccounts.find(
({ account: knownAccount }) => account === knownAccount,
);
const modifiedTimestamp = Number(localTimestamp) * 1000;
const modifiedDate = new Date(modifiedTimestamp);
return (
<Row
key={index}
justify="space-between"
align="middle"
gutter={[12, 12]}
>
<Col
xs={natricons ? 12 : 24}
md={4}
lg={2}
className="gutter-row"
span={6}
>
<Tooltip
placement="right"
title={
typeof confirmed !== "undefined"
? t(
`pages.block.${
toBoolean(confirmed) === false
? "pending"
: "confirmed"
}Status`,
)
: null
}
>
<Tag
// @ts-ignore
color={TwoToneColors[themeColor]}
style={{ textTransform: "capitalize" }}
className={`tag-${subtype || type}`}
icon={
typeof confirmed !== "undefined" ? (
toBoolean(confirmed) === false ? (
<SyncOutlined spin />
) : (
<CheckCircleOutlined />
)
) : null
}
>
{t(`transaction.${transactionType}`)}
</Tag>
</Tooltip>
</Col>
{natricons ? (
<Col xs={12} md={2} style={{ textAlign: "right" }}>
<Natricon
account={account}
style={{
margin: "-12px -6px -18px -18px ",
width: `${smallNatriconSize ? 60 : 80}px`,
height: `${smallNatriconSize ? 60 : 80}px`,
}}
/>
</Col>
) : null}
<Col
xs={24}
md={natricons ? 18 : 20}
lg={natricons ? 12 : 14}
>
{knownAccount ? (
<div className="color-important">
{knownAccount.alias}
</div>
) : null}
{account ? (
<Link
to={`/account/${account}`}
className="break-word color-normal"
>
{account}
</Link>
) : (
t("common.notAvailable")
)}
<br />
<Link
to={`/block/${hash}`}
className="color-muted truncate"
>
{hash}
</Link>
</Col>
<Col xs={16} md={12} lg={5}>
<Text
// @ts-ignore
style={{ color: Colors[themeColor] }}
className="break-word"
>
{!amount || amount === "0"
? t("common.notAvailable")
: ""}
{amount && amount !== "0"
? `Ӿ ${new BigNumber(rawToRai(amount)).toFormat()}`
: ""}
</Text>
</Col>
<Col xs={8} md={12} lg={3} style={{ textAlign: "right" }}>
{Number(localTimestamp) ? (
<>
{modifiedDate.getFullYear()}/
{String(modifiedDate.getMonth() + 1).padStart(2, "0")}/
{String(modifiedDate.getDate()).padStart(2, "0")}
<br />
<TimeAgo
locale={i18next.language}
style={{ fontSize: "12px" }}
className="color-muted"
datetime={modifiedTimestamp}
live={false}
/>
</>
) : (
t("common.unknown")
)}
</Col>
</Row>
);
},
)}
{showPaginate ? (
<Row className="row-pagination">
{isPaginated ? (
<Col xs={24} style={{ textAlign: "right" }}>
<Pagination
size="small"
{...{
total: totalPages,
pageSize,
current: currentPage,
disabled: false,
onChange: (page: number) => {
if (scrollTo) {
const element = document.getElementById(scrollTo);
element?.scrollIntoView();
}
setCurrentPage?.(page);
},
showSizeChanger: false,
}}
/>
</Col>
) : null}
{!isPaginated && setCurrentHead ? (
<Col xs={24} style={{ textAlign: "center" }}>
<Button
// @ts-ignore
onClick={setCurrentHead}
type={theme === Theme.DARK ? "primary" : "default"}
>
{t("pages.account.loadMoreTransactions")}
</Button>
</Col>
) : null}
</Row>
) : null}
</>
) : (
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
style={{ padding: "12px" }}
/>
)}
</Card>
);
}
Example #14
Source File: index.tsx From dashboard with Apache License 2.0 | 4 votes |
StaffList: React.FC = () => {
const [syncLoading, setSyncLoading] = useState<boolean>(false);
const [currentDepartment, setCurrentDepartment] = useState('0');
const actionRef = useRef<ActionType>();
const getDepartmentKey = (key: string) => {
setCurrentDepartment(key)
}
const columns: ProColumns<StaffList.Item>[] = [
{
title: 'ID',
dataIndex: 'id',
valueType: 'text',
hideInTable: true,
hideInSearch: true,
},
{
title: '员工',
dataIndex: 'name',
valueType: 'text',
hideInSearch: false,
render: (dom, item) => {
return (
<Space>
<div className={'tag-like-staff-item'}>
<img src={item.avatar_url} className={'icon'} alt={item.name}/>
<span className={'text'}>{item.name}</span>
</div>
</Space>
);
},
},
{
title: '所在部门',
dataIndex: 'departments',
valueType: 'text',
hideInSearch: true,
render: (dom) => {
// @ts-ignore
const arr = dom?.length > 1 ? dom?.slice(1) : dom;
return (
<Space>
{arr?.map((i: any) => (
<span key={i.id}>{i.name}</span>
))}
</Space>
);
},
},
{
title: '角色',
dataIndex: 'role_type',
order: 100,
hideInSearch: false,
valueType: 'select',
valueEnum: {
'': {text: '全部账号', role_type: ''},
superAdmin: {text: '超级管理员', role_type: 'superAdmin'},
admin: {text: '管理员', role_type: 'admin'},
departmentAdmin: {text: '部门管理员', role_type: 'departmentAdmin'},
staff: {text: '普通员工', role_type: 'staff'},
},
},
{
title: '授权状态',
dataIndex: 'external',
valueType: 'text',
hideInSearch: true,
},
{
title: '操作',
hideInSearch: true,
width: 180,
render: (text, dom) => {
return (
<Space>
{
dom.enable_msg_arch === 2 ? <Tooltip placement="topLeft" title="该员工暂未开启消息存档">
<Button type={'link'} disabled={true}>聊天记录</Button>
</Tooltip>
:
<Button type={'link'}
onClick={() => history.push(`/staff-admin/corp-risk-control/chat-session?staff=${dom.ext_staff_id}`)}
>聊天记录</Button>
}
<Button type={'link'}
onClick={() => history.push(`/staff-admin/company-management/role?ext_staff_id=${dom.ext_staff_id}`)}>管理权限</Button>
</Space>
);
},
},
];
return (
<PageContainer
fixedHeader
header={{
title: '员工管理',
}}
extra={[
<Button
key={'sync'}
type="dashed"
icon={<SyncOutlined style={{fontSize: 16, verticalAlign: '-3px'}}/>}
loading={syncLoading}
onClick={async () => {
setSyncLoading(true);
const res: CommonResp = await Sync();
if (res.code === 0) {
setSyncLoading(false);
// @ts-ignore
actionRef?.current.reset();
message.success('同步成功');
} else {
setSyncLoading(false);
message.error(res.message);
}
}}
>
同步数据
</Button>,
]}
>
{!localStorage.getItem('customerLossMemberManagementTipsClosed') && (
<Alert
showIcon={true}
closable={true}
style={{marginBottom: 16}}
type="info"
message={
<Text type={'secondary'}>
部门数据与企业微信同步,若需要修改员工部门请前往企业微信设置
<Button type={'link'} onClick={()=>window.open('https://work.weixin.qq.com/wework_admin/loginpage_wx?from=myhome')}>去设置</Button>
</Text>
}
onClick={() => {
localStorage.setItem('customerLossMemberManagementTipsClosed', '1');
}}
/>
)}
<ProTable
actionRef={actionRef}
className={'table'}
scroll={{x: 'max-content'}}
columns={columns}
rowKey="id"
pagination={{
pageSizeOptions: ['5', '10', '20', '50', '100'],
pageSize: 5,
}}
toolBarRender={false}
bordered={false}
tableAlertRender={false}
tableRender={(_, dom) => (
<div className={styles.mixedTable}>
<div className={styles.leftPart}>
<DepartmentTree callback={getDepartmentKey}/>
</div>
<div className={styles.rightPart}>
<div className={styles.tableWrap}>{dom}</div>
</div>
</div>
)}
params={{
ext_department_ids: currentDepartment !== '0' ? currentDepartment : '0',
}}
request={async (params, sort, filter) => {
return ProTableRequestAdapter(params, sort, filter, QueryStaffsList);
}}
dateFormatter="string"
/>
</PageContainer>
);
}
Example #15
Source File: ScriptModal.tsx From dashboard with Apache License 2.0 | 4 votes |
ScriptModal: (props: ScriptModalProps, ref: any) => JSX.Element = (props, ref) => {
const formRef = useRef<FormInstance>();
const [fileInfoAry, setFileInfoAry] = useState<{ fileName: string, fileSize: string, key: number }[]>([]) // PDF的文件信息数组
const groupModalRef = useRef<any>({});
const [groupModalVisible, setGroupModalVisible] = useState(false);
const [groupItems, setGroupItems] = useState<Partial<ScriptGroup.Item>[]>([]);
const [groupItemsTimestamp, setGroupItemsTimestamp] = useState(Date.now);
const [deletedIds,setDeletedIds] = useState<number[]>([])
React.useImperativeHandle(ref, () => {
return {
open: (item: Partial<Script.Item>) => {
setGroupItemsTimestamp(Date.now)
setTimeout(() => {
formRef?.current?.setFieldsValue(item)
}, 100)
props.setVisible(true)
},
close: () => {
formRef?.current?.resetFields()
props.setVisible(false)
}
}
})
useEffect(() => {
QueryEnterpriseScriptGroups({page_size: 5000}).then(res => {
if (res?.code === 0 && res?.data) {
setGroupItems(res?.data?.items || [])
}
}).catch((err) => {
message.error(err);
});
}, [groupItemsTimestamp])
// 转为传给后端的数据
const transferParams = (params: any) => {
const newReplyDetails = []
for (let i = 0; i < params.reply_details.length; i += 1) {
const typeObjectKey = typeEnums[params.reply_details[i].content_type]
const replyDetailItem = {content_type: params.reply_details[i].content_type, id: params.reply_details[i].id || '',quick_reply_content: {}}
const item = params.reply_details[i]
if (typeObjectKey === 'text') {
const quickReplyContent = {
text: {
'content': item.text_content
}
}
replyDetailItem.quick_reply_content = quickReplyContent
newReplyDetails.push(replyDetailItem)
}
if (typeObjectKey === 'image') {
const quickReplyContent = {
image: {
'title': item.image_title,
'size': item.image_size,
'picurl': item.image_picurl
}
}
replyDetailItem.quick_reply_content = quickReplyContent
newReplyDetails.push(replyDetailItem)
}
if (typeObjectKey === 'link') {
const quickReplyContent = {
link: {
'title': item.link_title,
'picurl': item.link_picurl,
'desc': item.link_desc,
'url': item.link_url
}
}
replyDetailItem.quick_reply_content = quickReplyContent
newReplyDetails.push(replyDetailItem)
}
if (typeObjectKey === 'pdf') {
const quickReplyContent = {
pdf: {
'title': item.pdf_title,
'size': item.pdf_size,
'fileurl': item.pdf_fileurl
}
}
replyDetailItem.quick_reply_content = quickReplyContent
newReplyDetails.push(replyDetailItem)
}
if (typeObjectKey === 'video') {
const quickReplyContent = {
video: {
'title': item.video_title,
'size': item.video_size,
'picurl': item.video_picurl
}
}
replyDetailItem.quick_reply_content = quickReplyContent
newReplyDetails.push(replyDetailItem)
}
}
return {...params, reply_details: newReplyDetails, id: props.initialValues?.id,deleted_ids:deletedIds||[]}
}
return (
<div className={styles.scriptModalContainer}>
<Modal
{...props}
width={640}
className={'dialog from-item-label-100w'}
visible={props.visible}
onOk={() => {
setFileInfoAry([])
formRef?.current?.submit();
}}
onCancel={() => {
props.setVisible(false)
formRef?.current?.resetFields()
props.onCancel()
setFileInfoAry([])
}}
>
<ProForm
formRef={formRef}
layout={'horizontal'}
submitter={{
resetButtonProps: {
style: {
display: 'none',
},
},
submitButtonProps: {
style: {
display: 'none',
},
},
}}
onFinish={async (values) => {
const params: any = transferParams(values)
if (values.is_global === True) {
params.departments = [0];
}
await props.onFinish(params, props.initialValues?.id ? 'update' : 'create');
}}
>
<h3 className="dialog-title" style={{fontSize: 18}}>
{props.initialValues?.id ? '修改话术' : '新建话术'}
</h3>
<ProForm.Item
name={'group_id'}
label="分组"
rules={[{required: true, message: '请选择分组!'}]}
>
<Select
style={{width: 400}}
placeholder="请选择分组"
dropdownRender={menu => (
<div>
{menu}
<Divider style={{margin: '4px 0'}}/>
<div style={{display: 'flex', flexWrap: 'nowrap', padding: 8}}>
<a
style={{flex: 'none', display: 'block', cursor: 'pointer'}}
onClick={() => {
groupModalRef.current.open({sub_group: [{name: ''}]})
}}
>
<PlusOutlined/> 新建分组
</a>
</div>
</div>
)}
>
{
groupItems?.map(item => (
<Option key={item.id} value={String(item.id)}>
<FolderFilled style={{fontSize: '16px', color: '#138af8', marginRight: 8}}/>
{item.name}
</Option>
))
}
</Select>
</ProForm.Item>
<ProForm.Item name="name" label="标题" rules={[{required: true, message: '请输入标题!'}]} >
<Input placeholder="仅内部可见,方便整理和查看" style={{width: 400}} maxLength={10}/>
</ProForm.Item>
<ProFormList
name="reply_details"
creatorButtonProps={{
type: 'default',
style: {width: '128px', color: '#58adfc', borderColor: '#58adfc', marginLeft: '20px', marginTop: '-30px'},
position: 'bottom',
creatorButtonText: '添加内容',
}}
creatorRecord={{
content_type: 2,
}}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
itemRender={({listDom}, {field, record, operation}) => {
const currentKey = field.name;
// const boundKey = field.fieldKey;
return (
<div className={styles.dynamicFormContainer}>
<div>
<div className={styles.radioBox}>
<ProFormRadio.Group
name="content_type"
initialValue={'word'}
label=""
options={[
{
label: '文字',
value: 2,
},
{
label: '图片',
value: 3,
},
{
label: '网页',
value: 4,
},
{
label: 'PDF',
value: 5,
},
{
label: '视频',
value: 6,
},
]}
/>
</div>
<ProForm.Item name="id" label="" style={{display: 'none'}}>
<Input />
</ProForm.Item>
<div className={styles.tabContent}>
<ProFormDependency name={['content_type']}>
{({content_type}) => {
if (content_type === 2) {
return (
<ProFormTextArea
name="text_content"
label={'话术内容'}
placeholder="请输入话术内容"
rules={[{required: true, message: '请输入话术内容!'}]}
fieldProps={{showCount: true, maxLength: 1300, allowClear: true}}
/>
);
}
if (content_type === 3) {
return <div>
<ProForm.Item name="image_title" label="图片名称"
rules={[{required: true, message: '请输入图片名称!'}]}>
<Input placeholder="图片名称可用于搜索" style={{width: 328}}/>
</ProForm.Item>
<ProForm.Item name="image_size" label="" style={{display: 'none'}}>
<Input placeholder="仅内部可见,方便整理和查看"/>
</ProForm.Item>
<ProForm.Item
name='image_picurl'
label={'上传图片'}
>
{/* 上传图片 */}
<Uploader
fileType={'formImage'}
customRequest={async (req) => {
try {
const file = req.file as RcFile;
const getUploadUrlRes = await GetSignedUrl({file_name: file.name})
if (getUploadUrlRes.code !== 0) {
message.error('获取上传地址失败');
return;
}
const uploadRes = await fetch(getUploadUrlRes?.data?.upload_url, {
method: 'PUT',
body: file
});
if (uploadRes.clone().ok) {
const reply_details = formRef?.current?.getFieldValue('reply_details');
reply_details[currentKey].image_picurl = getUploadUrlRes?.data?.download_url;
reply_details[currentKey].image_title = file.name;
reply_details[currentKey].image_size = String(file.size);
formRef?.current?.setFieldsValue({...reply_details})
return;
}
message.error('上传图片失败');
return;
} catch (e) {
message.error('上传图片失败');
}
}}
/>
</ProForm.Item>
</div>
}
if (content_type === 4) {// 解析链接
return (
<div>
<ProFormText
name='link_url'
label='链接地址'
placeholder="请输入链接,链接地址以http或https开头"
fieldProps={{
addonAfter: (
<Tooltip title="点击抓取远程链接,自动填充标题,描述,图片">
<div
onClick={async () => {
const res = await ParseURL(formRef?.current?.getFieldValue('reply_details')[currentKey].link_url)
if (res.code !== 0) {
message.error(res.message);
} else {
message.success('解析链接成功');
const reply_details = formRef?.current?.getFieldValue('reply_details');
reply_details[currentKey].link_title = res.data.title;// 链接标题
reply_details[currentKey].link_desc = res.data.desc; // 链接描述
reply_details[currentKey].link_picurl = res.data.img_url; // 图片
formRef?.current?.setFieldsValue({reply_details})
}
}}
style={{
cursor: "pointer",
width: 32,
height: 30,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<SyncOutlined/>
</div>
</Tooltip>
)
}}
rules={[
{
required: true,
message: '请输入链接地址',
},
{
type: 'url',
message: '请填写正确的的URL,必须是http或https开头',
},
]}
/>
<ProFormText
name='link_title'
label='链接标题'
width='md'
rules={[
{
required: true,
message: '请输入链接标题',
},
]}
/>
<ProFormTextArea
name='link_desc'
label='链接描述'
width='md'
/>
<ProForm.Item
label='链接封面'
name='link_picurl'
rules={[
{
required: true,
message: '请上传链接图片!',
},
]}
>
<Uploader
fileType={'formImage'}
customRequest={async (req) => {
try {
const file = req.file as RcFile;
const getUploadUrlRes = await GetSignedUrl({file_name: file.name})
if (getUploadUrlRes.code !== 0) {
message.error('获取上传地址失败');
return;
}
const uploadRes = await fetch(getUploadUrlRes?.data?.upload_url, {
method: 'PUT',
body: file
});
if (uploadRes.clone().ok) {
const reply_details = formRef?.current?.getFieldValue('reply_details');
reply_details[currentKey].link_picurl = getUploadUrlRes?.data?.download_url;
formRef?.current?.setFieldsValue({...reply_details})
return;
}
message.error('上传图片失败');
return;
} catch (e) {
message.error('上传图片失败');
}
}}
/>
</ProForm.Item>
</div>
)
}
if (content_type === 5) {
return (
<div>
<ProForm.Item name="pdf_title" label="" style={{display: 'none'}}>
<Input/>
</ProForm.Item>
<ProForm.Item name="pdf_size" label="" style={{display: 'none'}}>
<Input/>
</ProForm.Item>
<div className={styles.pdfUploadBox}>
<ProForm.Item name={'pdf_fileurl'}>
<Uploader
fileType='PDF'
fileInfoAry={fileInfoAry}
setFileInfoAry={setFileInfoAry}
currentKey={currentKey}
initialFileInfo={{
// 修改PDF时要回显的文件信息
fileName: formRef?.current?.getFieldValue('reply_details')[currentKey].pdf_title,
fileSize: formRef?.current?.getFieldValue('reply_details')[currentKey].pdf_size,
key: currentKey
}}
customRequest={async (req) => {
try {
const file = req.file as RcFile;
const getUploadUrlRes = await GetSignedUrl({file_name: file.name})
if (getUploadUrlRes.code !== 0) {
message.error('获取上传地址失败');
return;
}
// 上传
const uploadRes = await fetch(getUploadUrlRes?.data?.upload_url, {
method: 'PUT',
body: file
});
// 下载
if (uploadRes.clone().ok) {
const reply_details = formRef?.current?.getFieldValue('reply_details');
reply_details[currentKey].pdf_fileurl = getUploadUrlRes?.data?.download_url;
reply_details[currentKey].pdf_title = file.name;
reply_details[currentKey].pdf_size = String(file.size);
formRef?.current?.setFieldsValue({reply_details})
return;
}
message.error('上传PDF失败');
return;
} catch (e) {
message.error('上传PDF失败');
}
}}
/>
</ProForm.Item>
</div>
</div>
);
}
if (content_type === 6) {
return (
<>
<Row>
<div style={{display: 'none'}}>
<ProFormText name="video_title"/>
<ProFormText name="video_size"/>
</div>
<div className={styles.videoUplaodBox}></div>
<ProForm.Item name={'video_picurl'}>
<Uploader
fileType='视频'
customRequest={async (req) => {
try {
const file = req.file as RcFile;
const getUploadUrlRes = await GetSignedUrl({file_name: file.name})
if (getUploadUrlRes.code !== 0) {
message.error('获取上传地址失败');
return;
}
// 上传
const uploadRes = await fetch(getUploadUrlRes?.data?.upload_url, {
method: 'PUT',
body: file
});
// 下载
if (uploadRes.clone().ok) {
const reply_details = formRef?.current?.getFieldValue('reply_details');
reply_details[currentKey].video_picurl = getUploadUrlRes?.data?.download_url;
reply_details[currentKey].video_title = file.name;
reply_details[currentKey].video_size = String(file.size);
formRef?.current?.setFieldsValue({reply_details})
return;
}
message.error('上传视频失败');
return;
} catch (e) {
message.error('上传视频失败');
}
}}
/>
</ProForm.Item>
</Row>
</>
);
}
return <></>;
}}
</ProFormDependency>
</div>
</div>
<div>
{
formRef?.current?.getFieldValue('reply_details').length>1 && <Tooltip key={'remove'} title="删除">
<DeleteTwoTone
style={{paddingTop: 8}}
className={'ant-pro-form-list-action-icon'}
onClick={() => {
const temp = [...fileInfoAry]
for (let i = 0; i < temp.length; i += 1) {
if (temp[i].key === currentKey) {
temp.splice(i, 1)
}
}
setFileInfoAry(temp)
if(formRef?.current?.getFieldValue('reply_details')?.[currentKey].id){
setDeletedIds([...deletedIds,formRef?.current?.getFieldValue('reply_details')[currentKey].id])
}
operation.remove(currentKey);
}}
/>
</Tooltip>
}
</div>
</div>
)
}}
>
</ProFormList>
</ProForm>
</Modal>
<GroupModal
allDepartments={props.allDepartments as DepartmentOption[]}
ref={groupModalRef}
visible={groupModalVisible}
setVisible={setGroupModalVisible}
onFinish={async (values, action) => {
if (action === 'create') {
await HandleRequest({...values, sub_groups: values.sub_group || []}, CreateEnterpriseScriptGroups, () => {
setGroupItemsTimestamp(Date.now)
groupModalRef.current.close()
})
} else {
await HandleRequest({...values, sub_groups: values.sub_group || []}, UpdateEnterpriseScriptGroups, () => {
setGroupItemsTimestamp(Date.now)
groupModalRef.current.close()
})
}
}}
/>
</div>
)
}
Example #16
Source File: index.tsx From dashboard with Apache License 2.0 | 4 votes |
Material: React.FC<MaterialProps> = (props) => {
const [materalList, setMateralList] = useState<Material.Item[]>([])
const [filterVisible, setFilterVisible] = useState<boolean>(false)
const [modalFormVisible, setModalFormVisible] = useState<boolean>(false)
const [targetUpdateMaterial, setTargetUpdateMaterial] = useState<Material.Item>({} as Material.Item)
const [targetDeleteMaterial, setTargetDeleteMaterial] = useState<Material.Item>({} as Material.Item)
const [tagModalVisible, setTagModalVisible] = useState(false)
const [choosedTags, setChoosedTags] = useState<MaterialTag.Item[]>([])// 新增&修改素材时选择的标签
const [searchTags, setSearchTags] = useState<MaterialTag.Item[]>([])
const realSearchTags = useRef<MaterialTag.Item[]>([])
const [tagsForFilter, setTagsForFilter] = useState<MaterialTag.Item[]>([]) // 筛选标签的dropdown里的所有标签
const [keyword, setKeyword] = useState<string>('');
const [showSearchTags, setShowSearchTags] = useState(false)
const [linkFetching, setLinkFetching] = useState(false);
const [fileInfo, setFileInfo] = useState({fileName: '', fileSize: ''})
const [materialLoading, setMaterialLoading] = useState(true)
const modalFormRef = useRef<FormInstance>()
const {allTags, allTagsMap} = useContext(TagContext)
useEffect(() => {
const filteredTags = allTags.filter((tag) => {
if (keyword.trim() === '') {
return true;
}
if (tag.name?.includes(keyword)) {
return true;
}
return false;
});
setTagsForFilter(filteredTags);
}, [allTags, keyword])
// 查询素材列表
const queryMaterialList = (material_tag_list?: string[], title?: string) => {
QueryMaterialList({
page_size: 5000,
material_type: fileTypeMap[props.fileType].material_type,
material_tag_list,
title
}).then(res => {
setMaterialLoading(false)
if (res?.code === 0 && res?.data) {
setMateralList(res?.data?.items || [])
} else {
message.error(res?.message)
}
})
}
useEffect(() => {
setMaterialLoading(true)
queryMaterialList()
realSearchTags.current = []
setSearchTags([])
}, [props.fileType, allTags])
// 处理修改 删除的目标素材
const operateMaterial = (targetMaterial: Material.Item, operation: string) => {
setFileInfo({fileName: targetMaterial.title, fileSize: targetMaterial.file_size})
if (operation === 'update') {
setModalFormVisible(true)
setTargetUpdateMaterial(targetMaterial)
setChoosedTags(targetMaterial?.material_tag_list?.map((tagId: string) =>
allTagsMap[tagId]
))
setTimeout(() => {
modalFormRef.current?.setFieldsValue({
...targetMaterial,
file_url: targetMaterial.url,
file_size: targetMaterial.file_size
})
}, 100)
}
if (operation === 'delete') {
setTargetDeleteMaterial(targetMaterial)
}
}
return (
<TagContext.Consumer>
{
(contextValue) => (
<div>
<div>
<div className={styles.topNav}>
<div className={styles.topNavTitle}>
{props.fileType}素材(共{materalList?.length}篇)
</div>
<div className={styles.topNavOperator}>
<Input.Search placeholder={`搜索${props.fileType}标题`} style={{width: 300}} onSearch={(value) => {
queryMaterialList(realSearchTags.current.map(tag => tag?.id), value)
}}/>
<Dropdown
visible={filterVisible}
overlay={
<div className={styles.overlay}>
<div className={styles.overlayTitle}>素材标签 ( {contextValue.allTags.length} )</div>
{/* 筛选标签 */}
<Form
layout={'horizontal'}
>
<Input
allowClear={true}
placeholder={'输入关键词搜索标签'}
value={keyword}
onChange={(e) => {
setKeyword(e.currentTarget.value)
}}
style={{width: 320, marginLeft: 10}}
/>
<div style={{padding: "14px 4px"}}>
{tagsForFilter?.map((tag) => {
const isSelected = searchTags.map((searchTag) => searchTag.id)?.includes(tag?.id);
return (
<Space direction={'horizontal'} wrap={true}>
<Tag
className={`tag-item ${isSelected ? ' selected-tag-item' : ''}`}
style={{cursor: 'pointer', margin: '6px'}}
key={tag.id}
onClick={() => {
if (tag?.id && isSelected) {
setSearchTags(searchTags.filter((searchTag) => {
return searchTag.id !== tag?.id
}))
} else {
setSearchTags([...searchTags, tag])
}
}}
>
{tag.name}
</Tag>
</Space>
)
})}
</div>
{contextValue.allTags?.length === 0 &&
<Empty style={{marginTop: 36, marginBottom: 36}} image={Empty.PRESENTED_IMAGE_SIMPLE}/>}
<div style={{display: 'flex', justifyContent: 'flex-end'}}>
<Button onClick={() => setFilterVisible(false)}>取消</Button>
<Button
style={{marginLeft: 6}}
type='primary'
htmlType="submit"
onClick={() => {
setFilterVisible(false)
setShowSearchTags(true)
realSearchTags.current = searchTags
queryMaterialList(realSearchTags.current.map(tag => tag?.id) || [])
}}>完成</Button>
</div>
</Form>
</div>
} trigger={['click']}>
<div>
<Button
style={{margin: '0px 6px'}}
onClick={() => {
setFilterVisible(!filterVisible)
}}>筛选</Button>
</div>
</Dropdown>
<Button type={'primary'} onClick={() => {
setModalFormVisible(true)
}}>添加{props.fileType}</Button>
</div>
</div>
</div>
<div>
{
realSearchTags.current.length > 0 && showSearchTags ? <div className={styles.filterTagBox}>
{
realSearchTags.current.map(tag =>
<Tag
key={tag.id}
className={'tag-item selected-tag-item'}
>
{tag.name}
<span>
<CloseOutlined
style={{fontSize: '12px', cursor: 'pointer'}}
onClick={() => {
realSearchTags.current = realSearchTags.current.filter((t) => {
return t.id !== tag?.id
})
setSearchTags(realSearchTags.current)
queryMaterialList(realSearchTags.current.map(t => t?.id) || [])
}}
/>
</span>
</Tag>
)
}
<Button
type={'link'}
icon={<ClearOutlined/>}
style={{display: (showSearchTags && realSearchTags.current.length > 0) ? 'inline-block' : 'none'}}
onClick={() => {
setShowSearchTags(false)
setSearchTags([])
queryMaterialList()
}}>清空筛选</Button>
</div>
:
<div style={{margin: 0}}/>
}
{/* 素材列表 */}
<Spin spinning={materialLoading} style={{marginTop:50}}>
<div className={styles.articles}>
{
materalList?.map((item) => <MaterialCard {...item} callback={operateMaterial}/>)
}
</div>
</Spin>
{
materalList?.length === 0 && !materialLoading && <Empty style={{marginTop: 100}} image={Empty.PRESENTED_IMAGE_SIMPLE}/>
}
</div>
{/* 修改素材弹窗 */}
<ModalForm
formRef={modalFormRef}
className={'dialog from-item-label-100w'}
layout={'horizontal'}
width={'560px'}
visible={modalFormVisible}
onVisibleChange={(visible) => {
if (!visible) {
setTargetUpdateMaterial({} as Material.Item)
setChoosedTags([])
}
setModalFormVisible(visible)
modalFormRef.current?.resetFields()
}}
// @ts-ignore
onFinish={(params) => {
if (targetUpdateMaterial.id) {
const tagIdList = choosedTags.map(tag => {
return tag?.id
})
UpdateMaterial({
material_type: fileTypeMap[props.fileType].material_type, ...params,
id: targetUpdateMaterial.id,
material_tag_list: choosedTags.length > 0 ? tagIdList : [],
})
.then(res => {
if (res?.code === 0) {
message.success(`修改${props.fileType}成功`)
unstable_batchedUpdates(() => {
queryMaterialList(searchTags.map(tag => tag?.id) || [])
setModalFormVisible(false)
setChoosedTags([])
setTargetUpdateMaterial({} as Material.Item)
})
} else {
message.error(res.message)
}
})
} else {
const tagIdList = choosedTags.map(tag => {
return tag?.id
})
CreateMaterial({
material_type: fileTypeMap[props.fileType].material_type, ...params,
material_tag_list: choosedTags.length > 0 ? tagIdList : [],
})
.then(res => {
if (res?.code === 0) {
message.success(`新增${props.fileType}成功`)
unstable_batchedUpdates(() => {
queryMaterialList(searchTags.map(tag => tag?.id) || [])
setModalFormVisible(false)
setChoosedTags([])
})
} else {
message.error(res.message)
}
})
}
}}
>
{
// 修改链接素材 弹窗内容
props.fileType === '链接' && <div key={props.fileType}>
<Spin spinning={linkFetching}>
<h2 className='dialog-title'> {targetUpdateMaterial.id ? '修改链接' : '添加链接'} </h2>
<ProForm.Item initialValue={'link'} name={'msgtype'} noStyle={true}>
<input type={'hidden'}/>
</ProForm.Item>
<ProFormText
name='link'
label='链接地址'
placeholder="链接地址以http(s)开头"
width='md'
fieldProps={{
disabled: linkFetching,
addonAfter: (
<Tooltip title="点击抓取远程链接,自动填充标题,描述,图片">
<div
onClick={async () => {
setLinkFetching(true);
const res = await ParseURL(modalFormRef.current?.getFieldValue('link'))
setLinkFetching(false);
if (res.code !== 0) {
message.error(res.message);
} else {
message.success('解析链接成功');
modalFormRef?.current?.setFieldsValue({
customer_link_enable: 1,
title: res.data.title,
digest: res.data.desc,
file_url: res.data.img_url,
})
}
}}
style={{
cursor: "pointer",
width: 32,
height: 30,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<SyncOutlined/>
</div>
</Tooltip>
)
}}
rules={[
{
required: true,
message: '请输入链接地址',
},
{
type: 'url',
message: '请填写正确的的URL,必须是http或https开头',
},
]}
/>
<ProFormText
name='title'
label='链接标题'
width='md'
rules={[
{
required: true,
message: '请输入链接标题',
},
]}
/>
<ProFormTextArea
name='digest'
label='链接描述'
width='md'
/>
<Form.Item
label='链接封面'
name='file_url'
rules={[
{
required: true,
message: '请上传链接图片!',
},
]}
>
<Uploader
fileType='formImage'
fileInfo={fileInfo}
setFileInfo={setFileInfo}
customRequest={async (req) => {
try {
const file = req.file as RcFile;
const getUploadUrlRes = await GetSignedUrl({file_name: file.name})
if (getUploadUrlRes.code !== 0) {
message.error('获取上传地址失败');
return;
}
// 上传
const uploadRes = await fetch(getUploadUrlRes?.data?.upload_url, {
method: 'PUT',
body: file
});
if (uploadRes.clone().ok) {
modalFormRef?.current?.setFieldsValue({
file_url: getUploadUrlRes?.data?.download_url,
file_size: String(file.size)
})
return;
}
message.error('上传图片失败');
return;
} catch (e) {
message.error('上传图片失败');
}
}}
/>
</Form.Item>
<div style={{display: 'none'}}>
<ProFormText name='file_size'/>
</div>
</Spin>
</div>
}
{
// 修改海报素材 弹窗内容
props.fileType === '海报' && <div key={props.fileType}>
<h2 className='dialog-title'> {targetUpdateMaterial.id ? '修改海报' : '添加海报'} </h2>
<ProForm.Item
name='file_url'
>
<Uploader
fileType='海报'
fileInfo={fileInfo}
setFileInfo={setFileInfo}
customRequest={async (req) => {
try {
const file = req.file as RcFile;
const getUploadUrlRes = await GetSignedUrl({file_name: file.name})
if (getUploadUrlRes.code !== 0) {
message.error('获取上传地址失败');
return;
}
// 上传
const uploadRes = await fetch(getUploadUrlRes?.data?.upload_url, {
method: 'PUT',
body: file
});
if (uploadRes.clone().ok) {
modalFormRef?.current?.setFieldsValue({
file_url: getUploadUrlRes?.data?.download_url,
file_size: String(file.size),
title: file.name
})
return;
}
message.error('上传图片失败');
return;
} catch (e) {
message.error('上传图片失败');
}
}}
/>
</ProForm.Item>
<div style={{display: 'none'}}>
<ProFormText name='file_size'/>
</div>
<div style={{display: 'none'}}>
{/* 文件名 */}
<ProFormText name='title'/>
</div>
</div>
}
{
// 修改视频素材 弹窗内容
props.fileType === '视频' && <div key={props.fileType}>
<h2 className='dialog-title'> {targetUpdateMaterial.id ? '修改视频' : '添加视频'} </h2>
<ProForm.Item
name='file_url'
>
<Uploader
fileType='视频'
fileInfo={fileInfo}
setFileInfo={setFileInfo}
customRequest={async (req) => {
try {
const file = req.file as RcFile;
const getUploadUrlRes = await GetSignedUrl({file_name: file.name})
if (getUploadUrlRes.code !== 0) {
message.error('获取上传地址失败');
return;
}
// 上传
const uploadRes = await fetch(getUploadUrlRes?.data?.upload_url, {
method: 'PUT',
body: file
});
if (uploadRes.clone().ok) {
modalFormRef?.current?.setFieldsValue({
file_url: getUploadUrlRes?.data?.download_url,
file_size: String(file.size),
title: file.name
})
return;
}
message.error('上传视频失败');
return;
} catch (e) {
message.error('上传视频失败');
}
}}
/>
</ProForm.Item>
<div style={{display: 'none'}}>
<ProFormText name='file_size'/>
</div>
<div style={{display: 'none'}}>
{/* 文件名 */}
<ProFormText name='title'/>
</div>
</div>
}
{
// 修改文件类素材 弹窗内容
(props.fileType === 'PDF' || props.fileType === 'PPT' || props.fileType === '文档' || props.fileType === '表格') &&
<div key={props.fileType}>
<h2
className='dialog-title'> {targetUpdateMaterial.id ? `修改${props.fileType}` : `添加${props.fileType}`} </h2>
<ProForm.Item
name='file_url'
>
<Uploader
fileType={props.fileType}
fileInfo={fileInfo}
setFileInfo={setFileInfo}
customRequest={async (req) => {
try {
const file = req.file as RcFile;
const getUploadUrlRes = await GetSignedUrl({file_name: file.name})
if (getUploadUrlRes.code !== 0) {
message.error('获取上传地址失败');
return;
}
// 上传
const uploadRes = await fetch(getUploadUrlRes?.data?.upload_url, {
method: 'PUT',
body: file
});
if (uploadRes.clone().ok) {
modalFormRef?.current?.setFieldsValue({
file_url: getUploadUrlRes?.data?.download_url,
file_size: String(file.size),
title: file.name
})
return;
}
message.error(`上传${props.fileType}失败`);
return;
} catch (e) {
message.error(`上传${props.fileType}失败`);
}
}}
/>
</ProForm.Item>
<div style={{display: 'none'}}>
<ProFormText name='file_size'/>
</div>
<div style={{display: 'none'}}>
{/* 文件名 */}
<ProFormText name='title'/>
</div>
</div>
}
<div className={styles.modalTagBox}>
<Space direction={'horizontal'} wrap={true}>
<Button icon={<PlusOutlined/>} onClick={() => setTagModalVisible(true)}>选择标签</Button>
{
choosedTags?.length > 0 && choosedTags?.map((tag) =>
<Tag
key={tag?.id}
className={'tag-item selected-tag-item'}
>
{tag?.name}
<span>
<CloseOutlined
style={{fontSize: '12px', cursor: 'pointer'}}
onClick={() => {
setChoosedTags(choosedTags?.filter((choosedTag) => {
return choosedTag?.id !== tag?.id
}))
}}
/>
</span>
</Tag>
)}
</Space>
</div>
</ModalForm>
{/* 删除素材 */}
<Modal
visible={!!targetDeleteMaterial.id}
onOk={() => {
DeleteMaterial({ids: [targetDeleteMaterial.id]}).then(res => {
if (res?.code === 0) {
message.success('删除素材标签成功')
// setListTimestamp(Date.now)
queryMaterialList(searchTags?.map(tag => tag?.id) || [])
} else {
message.success('删除失败')
}
setTargetDeleteMaterial({} as Material.Item)
})
}}
onCancel={() => {
setTargetDeleteMaterial({} as Material.Item)
}}
>
<h3>提示</h3>
<h4>确定删除「{(targetDeleteMaterial as Material.Item).title}」这个素材吗?删除后不可恢复</h4>
</Modal>
{/* 选择素材标签弹窗 */}
<TagModal
width={560}
isEditable={false}
defaultCheckedTags={() => {
if (choosedTags?.length > 0) {
return choosedTags
}
const tempArr: MaterialTag.Item[] = []
targetUpdateMaterial?.material_tag_list?.forEach((tagId: string) => {
tempArr.push(contextValue.allTagsMap[tagId])
});
return tempArr || []
}}
allTags={contextValue.allTags}
setAllTags={contextValue.setAllTags}
visible={tagModalVisible}
setVisible={setTagModalVisible}
onCancel={() => {
setChoosedTags([])
}}
reloadTags={contextValue.setTagsItemsTimestamp}
onFinish={async (values) => {
setChoosedTags(values)
}}
/>
</div>
)
}
</TagContext.Consumer>
);
}
Example #17
Source File: index.tsx From dashboard with Apache License 2.0 | 4 votes |
CustomerGroupsListList: React.FC = () => {
const [exportLoading, setExportLoading] = useState<boolean>(false);
const [extraFilterParams, setExtraFilterParams] = useState<any>();
const [allStaffs, setAllStaffs] = useState<StaffOption[]>([]);
const actionRef = useRef<ActionType>();
const queryFormRef = useRef<FormInstance>();
const [syncLoading, setSyncLoading] = useState<boolean>(false);
const [allTagGroups, setAllTagGroups] = useState<GroupChatTagGroupItem[]>([]);
const [selectedItems, setSelectedItems] = useState<GroupChatItem[]>([]);
const [batchTagModalVisible, setBatchTagModalVisible] = useState<boolean>(false);
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,
};
}) || [];
setAllStaffs(staffs);
} else {
message.error(res.message);
}
});
}, []);
useEffect(() => {
Query({ page_size: 5000 }).then((res) => {
if (res.code === 0) {
setAllTagGroups(res?.data?.items || []);
} else {
message.error(res.message);
}
});
}, []);
const formattedParams = (originParams: any) => {
const params = { ...originParams, ...extraFilterParams };
if (params.tag_list) {
params.group_tag_ids = params.tag_list;
delete params.tag_list;
}
if (params.create_time) {
[params.create_time_start, params.create_time_end] = params.create_time;
delete params.create_time;
}
return params;
};
const columns: ProColumns<GroupChatItem>[] = [
{
title: '群名称',
dataIndex: 'name',
valueType: 'text',
fixed:'left',
render: (dom, item) => {
return (
<div className={'tag-like-item'}>
<img className={'icon'} src={GroupChatIcon} />
<span className={'text'}>{item.name}</span>
</div>
);
},
},
{
title: '群主',
dataIndex: 'owners',
valueType: 'select',
renderFormItem: () => {
return (
<StaffTreeSelect options={allStaffs} maxTagCount={4} />
);
},
render: (__, item) => {
return (
<div className={'tag-like-item'}>
<img className={'icon'} src={item.owner_avatar_url} />
<span className={'text'}>{item.owner}</span>
</div>
);
},
},
{
title: '群标签',
dataIndex: 'group_tag_ids',
valueType: 'text',
renderFormItem: () => {
return (
<GroupChatTagSelect isEditable={false} allTagGroups={allTagGroups} maxTagCount={6} />
);
},
render: (dom, item) => {
return <CollapsedTags limit={6} tags={item.tags} />;
},
},
{
title: '群聊状态',
dataIndex: 'status',
valueType: 'select',
hideInTable: true,
hideInSearch: false,
valueEnum: {
0: '未解散',
1: '已解散',
},
},
{
title: '群人数',
dataIndex: 'total',
valueType: 'digit',
hideInSearch: true,
sorter: true,
render: (dom, item) => {
return <span>{item.total}</span>;
},
},
{
title: '当日入群',
dataIndex: 'today_join_member_num',
valueType: 'digit',
hideInSearch: true,
sorter: true,
render: (dom, item) => {
return <span>{item.today_join_member_num}</span>;
},
},
{
title: '当日退群',
dataIndex: 'today_quit_member_num',
valueType: 'digit',
hideInSearch: true,
sorter: true,
render: (dom, item) => {
return <span>{item.today_quit_member_num}</span>;
},
},
{
title: '创群时间',
dataIndex: 'create_time',
valueType: 'dateRange',
hideInSearch: false,
sorter: true,
filtered: true,
render: (dom, item) => {
return (
<div
dangerouslySetInnerHTML={{
__html: moment(item.create_time).format('YYYY-MM-DD HH:mm').split(' ').join('<br/>'),
}}
/>
);
},
},
{
title: '群ID',
dataIndex: 'ext_chat_id',
valueType: 'text',
hideInSearch: true,
render: (dom, item) => {
return (
<div className={'tag-like-item'}>
<span className={'text'}>{item.ext_chat_id}</span>
</div>
);
},
},
];
return (
<PageContainer
fixedHeader
header={{
title: '客户群列表',
}}
extra={[
<Button
key='export'
type='dashed'
loading={exportLoading}
icon={<CloudDownloadOutlined style={{ fontSize: 16, verticalAlign: '-3px' }} />}
onClick={async () => {
setExportLoading(true);
try {
const content = await ExportCustomerGroupsList(
formattedParams(queryFormRef.current?.getFieldsValue()),
);
const blob = new Blob([content], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
});
FileSaver.saveAs(blob, `客户群列表.xlsx`);
} catch (e) {
console.log(e);
message.error('导出失败');
}
setExportLoading(false);
}}
>
导出Excel
</Button>,
<Button
key={'sync'}
type='dashed'
icon={<SyncOutlined style={{ fontSize: 16, verticalAlign: '-3px' }} />}
loading={syncLoading}
onClick={async () => {
setSyncLoading(true);
const res: CommonResp = await Sync();
if (res.code === 0) {
setSyncLoading(false);
message.success('同步成功');
} else {
setSyncLoading(false);
message.error(res.message);
}
}}
>
同步数据
</Button>,
]}
>
<ProTable<GroupChatItem>
formRef={queryFormRef}
actionRef={actionRef}
className={'table'}
scroll={{ x: 'max-content' }}
columns={columns}
rowKey='id'
pagination={{
pageSizeOptions: ['5', '10', '20', '50', '100'],
pageSize: 5,
}}
onReset={() => {
setExtraFilterParams({});
}}
toolBarRender={false}
bordered={false}
tableAlertRender={false}
params={{}}
request={async (originParams: any, sort, filter) => {
return ProTableRequestAdapter(
formattedParams(originParams),
sort,
filter,
QueryCustomerGroupsList,
);
}}
dateFormatter='string'
rowSelection={{
onChange: (__, items) => {
setSelectedItems(items);
},
}}
/>
{selectedItems?.length > 0 && (
// 底部选中条目菜单栏
<FooterToolbar>
<span>
已选择 <a style={{ fontWeight: 600 }}>{selectedItems.length}</a> 项
</span>
<Divider type='vertical' />
<Button
icon={<TagOutlined />}
type={'dashed'}
onClick={() => {
setBatchTagModalVisible(true);
}}
>
批量打标签
</Button>
</FooterToolbar>
)}
<GroupChatTagSelectionModal
width={'630px'}
visible={batchTagModalVisible}
setVisible={setBatchTagModalVisible}
onFinish={async (selectedTags) => {
const selectedTagIDs = selectedTags.map((selectedTag) => selectedTag.id);
const selectedGroupChatIDs = selectedItems.map((groupChat) => groupChat.id);
await HandleRequest({
add_tag_ids: selectedTagIDs,
group_chat_ids: selectedGroupChatIDs,
}, UpdateGroupChatTags, () => {
// @ts-ignore
actionRef?.current?.reloadAndRest();
setSelectedItems([]);
});
}}
allTagGroups={allTagGroups}
isEditable={true}
withLogicalCondition={false}
/>
</PageContainer>
);
}
Example #18
Source File: index.tsx From dashboard with Apache License 2.0 | 4 votes |
CustomerTagGroupList: React.FC = () => {
const [currentItem, setCurrentItem] = useState<CustomerTagGroupItem>({});
const [tagGroups, setTagGroups] = useState<CustomerTagGroupItem[]>([]);
const [createModalVisible, setCreateModalVisible] = useState<boolean>(false);
const [editModalVisible, setEditModalVisible] = useState<boolean>(false);
const [syncLoading, setSyncLoading] = useState<boolean>(false);
const [actionLoading, setActionLoading] = useState<boolean>(false);
const [inputLoading, setInputLoading] = useState<boolean>(false);
const [minOrder, setMinOrder] = useState<number>(10000);
const [maxOrder, setMaxOrder] = useState<number>(100000);
const [currentInputTagGroupExtID, setCurrentInputTagGroupExtID] = useState<string>();
const [allDepartments, setAllDepartments] = useState<DepartmentOption[]>([]);
const [allDepartmentMap, setAllDepartmentMap] = useState<Dictionary<DepartmentOption>>({});
const queryFilterFormRef = useRef<FormInstance>();
useEffect(() => {
QueryDepartment({page_size: 5000}).then((res) => {
if (res.code === 0) {
const departments =
res?.data?.items?.map((item: DepartmentInterface) => {
return {
label: item.name,
value: item.ext_id,
...item,
};
}) || [];
setAllDepartments(departments);
setAllDepartmentMap(_.keyBy<DepartmentOption>(departments, 'ext_id'));
} else {
message.error(res.message);
}
});
queryFilterFormRef.current?.submit();
}, []);
// @ts-ignore
// @ts-ignore
return (
<PageContainer
fixedHeader
header={{
title: '客户标签管理',
}}
extra={[
<Button
key='create'
type='primary'
icon={<PlusOutlined style={{fontSize: 16, verticalAlign: '-3px'}}/>}
onClick={() => {
setCreateModalVisible(true);
}}
>
添加标签组
</Button>,
<Button
key={'sync'}
type='dashed'
icon={<SyncOutlined style={{fontSize: 16, verticalAlign: '-3px'}}/>}
loading={syncLoading}
onClick={async () => {
setSyncLoading(true);
const res: CommonResp = await Sync();
if (res.code === 0) {
setSyncLoading(false);
message.success('同步成功');
queryFilterFormRef.current?.submit();
} else {
setSyncLoading(false);
message.error(res.message);
}
}}
>
同步企业微信标签
</Button>,
]}
>
<ProCard className={styles.queryFilter}>
<QueryFilter
formRef={queryFilterFormRef}
onReset={() => {
queryFilterFormRef.current?.submit();
}}
onFinish={async (params: any) => {
setActionLoading(true);
const res: CommonResp = await Query({
...params,
page_size: 5000,
sort_field: 'order',
sort_type: 'desc',
});
setActionLoading(false);
if (res.code === 0) {
setTagGroups(res.data.items);
if (res.data?.items[0]) {
setMaxOrder(res.data.items[0]?.order);
}
if (res.data?.items.length >= 1 && res.data?.items[res.data?.items.length - 1]) {
let min = res.data?.items[res.data?.items.length - 1];
min = min - 1 >= 0 ? min - 1 : 0;
setMinOrder(min);
}
} else {
message.error('查询标签失败');
setTagGroups([]);
}
}}
>
<Form.Item label='可用部门' name='ext_department_ids'>
<DepartmentTreeSelect
onChange={() => {
queryFilterFormRef.current?.submit();
}}
options={allDepartments}
/>
</Form.Item>
<ProFormText width={'md'} name='name' label='搜索' placeholder='请输入关键词'/>
</QueryFilter>
</ProCard>
<ProCard style={{marginTop: 12}} bodyStyle={{paddingTop: 0}} gutter={0}>
<Spin spinning={actionLoading}>
{(!tagGroups || tagGroups.length === 0) && <Empty style={{marginTop: 36, marginBottom: 36}}/>}
{tagGroups && tagGroups.length > 0 && (
<ReactSortable<any>
handle={'.draggable-button'}
className={styles.tagGroupList}
list={tagGroups}
setList={setTagGroups}
swap={true}
onEnd={async (e) => {
// @ts-ignore
const from = tagGroups[e.newIndex];
// @ts-ignore
const to = tagGroups[e.oldIndex];
const res = await ExchangeOrder({id: from.id, exchange_order_id: to.id});
if (res.code !== 0) {
message.error(res.message)
}
}}
>
{tagGroups.map((tagGroup) => (
<Row className={styles.tagGroupItem} data-id={tagGroup.id} key={tagGroup.ext_id}>
<Col md={4} className={styles.tagName}>
<h4>{tagGroup.name}</h4>
</Col>
<Col md={16} className={styles.tagList}>
<Row>
可见范围:
{tagGroup.department_list && !tagGroup.department_list.includes(0) ? (
<Space direction={'horizontal'} wrap={true} style={{marginBottom: 6}}>
{tagGroup.department_list.map((id) => (
<div key={id}>
<span>
<FolderFilled
style={{
color: '#47a7ff',
fontSize: 20,
marginRight: 6,
verticalAlign: -6,
}}
/>
{allDepartmentMap[id]?.name}
</span>
</div>
))}
</Space>
) : (
<span>全部员工可见</span>
)}
</Row>
<Row style={{marginTop: 12}}>
<Space direction={'horizontal'} wrap={true}>
<Button
icon={<PlusOutlined/>}
onClick={() => {
setCurrentInputTagGroupExtID(tagGroup.ext_id);
}}
>
添加
</Button>
{currentInputTagGroupExtID === tagGroup.ext_id && (
<Input
autoFocus={true}
disabled={inputLoading}
placeholder='逗号分隔,回车保存'
onBlur={() => setCurrentInputTagGroupExtID('')}
onPressEnter={async (e) => {
setInputLoading(true);
const res = await CreateTag({
names: e.currentTarget.value
.replace(',', ',')
.split(',')
.filter((val) => val),
ext_tag_group_id: tagGroup.ext_id || '',
});
if (res.code === 0) {
setCurrentInputTagGroupExtID('');
tagGroup.tags?.unshift(...res.data);
} else {
message.error(res.message);
}
setInputLoading(false);
}}
/>
)}
{tagGroup.tags?.map((tag) => (
<Tag className={styles.tagItem} key={tag.id}>
{tag.name}
</Tag>
))}
</Space>
</Row>
</Col>
<Col md={4} className={styles.groupAction}>
<Tooltip title="拖动可实现排序" trigger={['click']}>
<Button
className={'draggable-button'}
icon={<DragOutlined
style={{cursor: 'grabbing'}}
/>}
type={'text'}
>
排序
</Button>
</Tooltip>
<Button
icon={<EditOutlined/>}
type={'text'}
onClick={() => {
setCurrentItem(tagGroup);
setEditModalVisible(true);
}}
>
修改
</Button>
<Button
icon={<DeleteOutlined/>}
type={'text'}
onClick={() => {
Modal.confirm({
title: `删除标签分组`,
content: `是否确认删除「${tagGroup.name}」分组?`,
okText: '删除',
okType: 'danger',
cancelText: '取消',
onOk() {
return HandleRequest({ext_ids: [tagGroup.ext_id]}, Delete, () => {
queryFilterFormRef.current?.submit();
});
},
});
}}
>
删除
</Button>
</Col>
</Row>
))}
</ReactSortable>
)}
</Spin>
</ProCard>
<CreateModalForm
// 创建标签
type={'create'}
minOrder={minOrder}
maxOrder={maxOrder}
allDepartments={allDepartments}
setVisible={setCreateModalVisible}
initialValues={{tags: [{name: ''}], department_list: [0]}}
visible={createModalVisible}
onFinish={async (values) => {
await HandleRequest(values, Create, () => {
queryFilterFormRef.current?.submit();
setCreateModalVisible(false);
});
}}
/>
<CreateModalForm
// 修改标签
type={'edit'}
destroyOnClose={true}
minOrder={minOrder}
maxOrder={maxOrder}
allDepartments={allDepartments}
setVisible={setEditModalVisible}
visible={editModalVisible}
initialValues={currentItem}
onFinish={async (values) => {
await HandleRequest(values, Update, () => {
queryFilterFormRef.current?.submit();
setEditModalVisible(false);
});
}}
/>
</PageContainer>
);
}
Example #19
Source File: index.tsx From dashboard with Apache License 2.0 | 4 votes |
CustomerList: React.FC = () => {
const [exportLoading, setExportLoading] = useState<boolean>(false);
const [extraFilterParams] = useState<any>();// setExtraFilterParams
const [allStaffs, setAllStaffs] = useState<StaffOption[]>([]);
const [staffMap, setStaffMap] = useState<Dictionary<StaffOption>>({});
const actionRef = useRef<ActionType>();
const queryFormRef = useRef<FormInstance>();
const [syncLoading, setSyncLoading] = useState<boolean>(false);
const [selectedItems, setSelectedItems] = useState<CustomerItem[]>([]);
const [allTagGroups, setAllTagGroups] = useState<CustomerTagGroupItem[]>([]);
const [batchTagModalVisible, setBatchTagModalVisible] = useState<boolean>(false);
const formattedParams = (originParams: any) => {
const params = {...originParams, ...extraFilterParams};
if (params.created_at) {
[params.start_time, params.end_time] = params.created_at;
delete params.created_at;
}
if (params.relation_create_at) {
[params.connection_create_start, params.connection_create_end] = params.relation_create_at;
delete params.relation_create_at;
}
if (params.add_way) {
params.channel_type = params.add_way;
delete params.add_way;
}
return params;
};
useEffect(() => {
QueryCustomerTagGroups({page_size: 5000}).then((res) => {
if (res.code === 0) {
setAllTagGroups(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,
};
}) || [];
setAllStaffs(staffs);
setStaffMap(_.keyBy<StaffOption>(staffs, 'ext_id'));
} else {
message.error(res.message);
}
});
}, []);
const columns: ProColumns<CustomerItem>[] = [
{
fixed: 'left',
title: '客户名',
dataIndex: 'name',
valueType: 'text',
render: (dom, item) => {
return (
<div className={'customer-info-field'}>
<a key='detail' onClick={() => {
history.push(`/staff-admin/customer-management/customer/detail?ext_customer_id=${item.ext_customer_id}`);
}}>
<img
src={item.avatar}
className={'icon'}
alt={item.name}
/>
</a>
<div className={'text-group'}>
<p className={'text'}>
{item.name}
</p>
{item.corp_name && (
<p className={'text'} style={{color: '#eda150'}}>@{item.corp_name}</p>
)}
{item.type === 1 && (
<p className={'text'} style={{color: '#5ec75d'}}>@微信</p>
)}
</div>
</div>
);
},
},
{
title: '添加人',
dataIndex: 'ext_staff_ids',
valueType: 'text',
width: 200,
renderFormItem: () => {
return (
<StaffTreeSelect options={allStaffs} maxTagCount={4}/>
);
},
render: (dom, item) => {
const staffs: StaffItem[] = [];
item?.staff_relations?.forEach((staff_relation) => {
// @ts-ignore
const staff = staffMap[staff_relation.ext_staff_id];
if (staff) {
staffs.push(staff);
}
});
return (
<CollapsedStaffs limit={2} staffs={staffs}/>
);
},
},
{
title: '标签',
dataIndex: 'ext_tag_ids',
valueType: 'text',
hideInSearch: false,
renderFormItem: () => {
return (
<CustomerTagSelect isEditable={false} allTagGroups={allTagGroups} maxTagCount={6}/>
);
},
render: (dom, item) => {
const tags: any[] = [];
item.staff_relations?.forEach((relation) => {
if (relation.ext_staff_id === localStorage.getItem('extStaffAdminID')) {
relation.customer_staff_tags?.forEach((tag) => {
tags.push(tag);
});
}
});
return <CollapsedTags limit={6} tags={tags}/>;
},
},
{
title: '添加时间',
dataIndex: 'created_at',
valueType: 'dateRange',
sorter: true,
filtered: true,
render: (dom, item) => {
if (item.staff_relations && item.staff_relations.length > 0) {
const staff_relation = item.staff_relations[0];
return (
<div className={styles.staffTag}
dangerouslySetInnerHTML={{
__html: moment(staff_relation.createtime)
.format('YYYY-MM-DD HH:mm')
.split(' ')
.join('<br />'),
}}
/>
);
}
return <></>;
},
},
{
title: '更新时间',
dataIndex: ' updated_at',
valueType: 'dateRange',
// sorter: true,
filtered: true,
hideInSearch: true,
render: (dom, item) => {
return (
<div
dangerouslySetInnerHTML={{
__html: moment(item.updated_at)
.format('YYYY-MM-DD HH:mm')
.split(' ')
.join('<br />'),
}}
/>
);
},
},
{
title: '添加渠道',
dataIndex: 'add_way',
valueType: 'select',
valueEnum: addWayEnums,
// width: 220,
render: (dom, item) => {
return <span>{item.staff_relations?.map((para) => {
return (`${addWayEnums[para.add_way || 0]}\n`);
})}</span>;
},
},
{
title: '性别',
dataIndex: 'gender',
valueType: 'select',
hideInTable: true,
valueEnum: {
1: '男',
2: '女',
3: '未知',
0: '不限',
},
},
{
title: '账号类型',
dataIndex: 'type',
valueType: 'select',
hideInTable: true,
valueEnum: {
1: '微信',
2: '企业微信',
0: '不限',
},
},
{
title: '流失状态',
dataIndex: 'out_flow_status',
valueType: 'select',
hideInTable: true,
hideInSearch: false,
tooltip: '员工授权后的流失客户',
valueEnum: {
1: '已流失',
2: '未流失',
},
},
{
title: '操作',
width: 120,
valueType: 'option',
render: (dom, item) => [
<a key='detail' onClick={() => {
history.push(`/staff-admin/customer-management/customer/detail?ext_customer_id=${item.ext_customer_id}`);
}}
>详情</a>,
],
},
];
return (
<PageContainer
fixedHeader
header={{
title: '客户管理',
}}
extra={[
<Button
key={'export'}
type='dashed'
loading={exportLoading}
icon={<CloudDownloadOutlined style={{fontSize: 16, verticalAlign: '-3px'}}/>}
onClick={async () => {
setExportLoading(true);
try {
const content = await ExportCustomer(
formattedParams(queryFormRef.current?.getFieldsValue()),
);
const blob = new Blob([content], {
type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
});
FileSaver.saveAs(blob, `客户数据列表.xlsx`);
} catch (e) {
console.log(e);
message.error('导出失败');
}
setExportLoading(false);
}}
>
导出Excel
</Button>,
<Button
key={'sync'}
type='dashed'
icon={<SyncOutlined style={{fontSize: 16, verticalAlign: '-3px'}}/>}
loading={syncLoading}
onClick={async () => {
setSyncLoading(true);
const res: CommonResp = await Sync();
if (res.code === 0) {
setSyncLoading(false);
message.success('同步成功');
} else {
setSyncLoading(false);
message.error(res.message);
}
}}
>
同步数据
</Button>,
]}
>
<ProTable
rowSelection={{
onChange: (__, items) => {
setSelectedItems(items);
},
}}
formRef={queryFormRef}
actionRef={actionRef}
className={'table'}
scroll={{x: 'max-content'}}
columns={columns}
rowKey='id'
pagination={{
pageSizeOptions: ['5', '10', '20', '50', '100'],
pageSize: 5,
}}
toolBarRender={false}
bordered={false}
tableAlertRender={false}
params={{}}
request={async (originParams: any, sort, filter) => {
const res = ProTableRequestAdapter(
formattedParams(originParams),
sort,
filter,
QueryCustomer,
);
console.log(await res);
return await res;
}}
dateFormatter='string'
/>
{selectedItems?.length > 0 && (
// 底部选中条目菜单栏
<FooterToolbar>
<span>
已选择 <a style={{fontWeight: 600}}>{selectedItems.length}</a> 项
</span>
<Divider type='vertical'/>
<Button
icon={<TagOutlined/>}
type={'dashed'}
onClick={() => {
setBatchTagModalVisible(true);
}}
>
批量打标签
</Button>
</FooterToolbar>
)}
<CustomerTagSelectionModal
width={'630px'}
visible={batchTagModalVisible}
setVisible={setBatchTagModalVisible}
onFinish={async (selectedTags) => {
const selectedExtTagIDs = selectedTags.map((selectedTag) => selectedTag.ext_id);
const selectedExtCustomerIDs = selectedItems.map((customer) => customer.ext_customer_id);
await HandleRequest({
add_ext_tag_ids: selectedExtTagIDs,
ext_customer_ids: selectedExtCustomerIDs,
}, UpdateCustomerTags, () => {
// @ts-ignore
actionRef?.current?.reloadAndRest();
setSelectedItems([]);
});
}}
allTagGroups={allTagGroups}
isEditable={true}
withLogicalCondition={false}
/>
</PageContainer>
);
}
Example #20
Source File: index.tsx From dashboard with Apache License 2.0 | 4 votes |
AutoReply: React.FC<AutoReplyProps> = (props) => {
const {welcomeMsg, setWelcomeMsg, isFetchDone} = props;
const [modalVisible, setModalVisible] = useState(false);
const [attachments, setAttachments] = useState<Attachment[]>([]);
const [currentIndex, setCurrentIndex] = useState<number>(0);
const [currentMode, setCurrentMode] = useState<MsgType>('image');
const [linkFetching, setLinkFetching] = useState(false);
const [content, setContent] = useState('');
const contentRef = useRef<React.RefObject<HTMLElement>>();
const imageModalFormRef = useRef<FormInstance>();
const linkModalFormRef = useRef<FormInstance>();
const miniAppModalFormRef = useRef<FormInstance>();
const UploadFileFn = async (req: UploadRequestOption, ref: MutableRefObject<any | undefined>, inputName: string) => {
const file = req.file as File;
if (!file.name) {
message.error('非法参数');
return;
}
const hide = message.loading('上传中');
try {
const res = await GetSignedURL(file.name)
const data = res.data as GetSignedURLResult
if (res.code === 0) {
const uploadRes = (await fetch(data.upload_url, {
method: 'PUT',
body: file
}));
hide();
if (uploadRes.ok && ref) {
ref.current?.setFieldsValue({[inputName]: data.download_url});
return;
}
message.error('上传图片失败');
return;
}
hide();
message.error('获取上传地址失败');
return;
} catch (e) {
message.error('上传图片失败');
console.log(e);
}
};
useEffect(() => {
const formData = itemDataToFormData(welcomeMsg);
setAttachments(formData.attachments || []);
setContent(formData.text || '');
}, [isFetchDone]);
useEffect(() => {
setWelcomeMsg({
text: content || '',
attachments: attachments || [],
});
}, [content, attachments]);
return (
<>
<div className={styles.replyEditor}>
<div className={'preview-container'}>
<div className={styles.replyEditorPreview}>
<img src={phoneImage} className='bg'/>
<div className='content'>
<ul className='reply-list'>
{content && (
<li><img
src={avatarDefault}/>
<div className='msg text' dangerouslySetInnerHTML={{__html: content}}/>
</li>
)}
{attachments && attachments.length > 0 && (
attachments.map((attachment) => {
if (attachment.msgtype === 'image') {
return (
<li key={attachment.id}>
<img src={avatarDefault}/>
<div className={`msg image`}>
<img src={attachment.image?.pic_url}/>
</div>
</li>
);
}
if (attachment.msgtype === 'link') {
return (
<li key={attachment.id}>
<img src={avatarDefault}/>
<div className='msg link'><p className='title'>{attachment.link?.title}</p>
<div className='link-inner'><p
className='desc'>{attachment.link?.desc}</p>
<img src={attachment.link?.picurl}/>
</div>
</div>
</li>
);
}
if (attachment.msgtype === 'miniprogram') {
return (
<li key={attachment.id}>
<img src={avatarDefault}/>
<div className='msg miniprogram'>
<p className='m-title'>
<IconFont
type={'icon-weixin-mini-app'}
style={{marginRight: 4, fontSize: 14}}
/>
{attachment.miniprogram?.title}
</p>
<img src={attachment.miniprogram?.pic_media_id}/>
<p className='l-title'>
<IconFont type={'icon-weixin-mini-app'} style={{marginRight: 4}}/>
小程序
</p>
</div>
</li>
);
}
return '';
})
)}
</ul>
</div>
</div>
</div>
<div className='text-area-container'>
<div className={styles.msgTextareaContainer} style={{border: 'none'}}>
{props.enableQuickInsert && (
<div className='insert-btn '>
<span
className='clickable no-select'
onClick={() => {
setContent(`${content}[客户昵称]`);
}}
>[插入客户昵称]</span>
</div>
)}
<div className='textarea-container '>
<ContentEditable
// @ts-ignore
innerRef={contentRef}
onKeyDown={(event) => {
if (event.key === 'Enter') {
document.execCommand('insertLineBreak');
event.preventDefault();
}
}}
className={'textarea'}
html={content}
onChange={(e) => {
setContent(e.target.value);
}}/>
<div className='flex-row align-side'>
<p className='text-cnt'>{content.length}/600</p>
</div>
</div>
</div>
</div>
<div className='option-area-container'>
{attachments && attachments.length > 0 && (
<ReactSortable handle={'.draggable-button'} tag='ul' className={'select-msg-options'} list={attachments} setList={setAttachments}>
{attachments.map((attachment, index) => (
<li key={attachment.id} className='flex-row'>
<span>
<MinusCircleOutlined
onClick={() => {
const items = [...attachments];
items.splice(index, 1);
setAttachments(items);
}}
/>
【{msgTypes[attachment.msgtype]}】:
<span
className='col-1'>{attachment?.name}</span>
</span>
<span className='d-action-container'>
<EditOutlined
onClick={() => {
setCurrentMode(attachment.msgtype);
imageModalFormRef.current?.setFieldsValue(attachment.image);
linkModalFormRef.current?.setFieldsValue(attachment.link);
miniAppModalFormRef.current?.setFieldsValue(attachment.miniprogram);
setCurrentIndex(index);
setModalVisible(true);
}}
/>
<DragOutlined
className={'draggable-button'}
style={{cursor: 'grabbing'}}
/>
</span>
</li>
))}
</ReactSortable>
)}
<div className='option-container'>
<Dropdown
placement='topLeft'
trigger={['click']}
overlay={(
<Menu style={{minWidth: 120}}>
<Menu.Item
key={'image'}
icon={<FileImageOutlined/>}
onClick={() => {
setCurrentMode('image');
setCurrentIndex(attachments.length);
imageModalFormRef.current?.resetFields();
setModalVisible(true);
}}
>
图片
</Menu.Item>
<Menu.Item
key={'link'}
icon={<LinkOutlined/>}
onClick={() => {
setCurrentMode('link');
setCurrentIndex(attachments.length);
setModalVisible(true);
}}
>
链接
</Menu.Item>
<Menu.Item
key={'miniApp'}
icon={<IconFont type={'icon-weixin-mini-app'}/>}
onClick={() => {
setCurrentMode('miniprogram');
setCurrentIndex(attachments.length);
setModalVisible(true);
}}
>
小程序
</Menu.Item>
</Menu>
)}
>
<a className='ant-dropdown-link' onClick={e => e.preventDefault()}>
<PlusCircleOutlined/> 添加附件
</a>
</Dropdown>
</div>
</div>
</div>
<ModalForm
formRef={imageModalFormRef}
className={'dialog from-item-label-100w'}
layout={'horizontal'}
width={'560px'}
visible={currentMode === 'image' && modalVisible}
onVisibleChange={setModalVisible}
onFinish={async (params: { title: string, pic_url: string, msgtype: MsgType }) => {
attachments[currentIndex] = {
id: new Date().getTime().toString(),
msgtype: params.msgtype,
name: params.title,
image: {...params},
};
setAttachments(attachments);
return true;
}}
>
<h2 className='dialog-title'> 添加图片附件 </h2>
<ProForm.Item initialValue={'image'} name={'msgtype'} noStyle={true}>
<input type={'hidden'}/>
</ProForm.Item>
<ProFormText
name='title'
label='图片名称'
placeholder={'请输入图片名称'}
width='md'
rules={[
{
required: true,
message: '请输入图片名称!',
},
]}
/>
<Form.Item
label='上传图片'
name='pic_url'
rules={[
{
required: true,
message: '请上传图片!',
},
]}
>
<ImageUploader
customRequest={async (req) => {
await UploadFileFn(req, imageModalFormRef, 'pic_url')
}}
/>
</Form.Item>
</ModalForm>
<ModalForm
formRef={linkModalFormRef}
className={'dialog from-item-label-100w'}
layout={'horizontal'}
width={'560px'}
visible={currentMode === 'link' && modalVisible}
onVisibleChange={setModalVisible}
onFinish={async (params) => {
attachments[currentIndex] = {
id: new Date().getTime().toString(),
msgtype: params.msgtype,
name: params.title,
// @ts-ignore
link: {...params},
};
setAttachments(attachments);
return true;
}}
>
<Spin spinning={linkFetching}>
<h2 className='dialog-title'> 添加链接附件 </h2>
<ProForm.Item initialValue={'link'} name={'msgtype'} noStyle={true}>
<input type={'hidden'}/>
</ProForm.Item>
<ProFormText
name='url'
label='链接地址'
width='md'
fieldProps={{
disabled: linkFetching,
addonAfter: (
<Tooltip title="点击抓取远程链接,自动填充标题,描述,图片">
<div
onClick={async () => {
setLinkFetching(true);
const res = await ParseURL(linkModalFormRef.current?.getFieldValue('url'))
setLinkFetching(false);
if (res.code !== 0) {
message.error(res.message);
} else {
message.success('解析链接成功');
linkModalFormRef?.current?.setFieldsValue({
customer_link_enable: 1,
title: res.data.title,
desc: res.data.desc,
picurl: res.data.img_url,
})
}
}}
style={{
cursor: "pointer",
width: 32,
height: 30,
display: 'flex',
alignItems: 'center',
justifyContent: 'center'
}}>
<SyncOutlined/>
</div>
</Tooltip>
)
}}
rules={[
{
required: true,
message: '请输入链接地址',
},
{
type: 'url',
message: '请填写正确的的URL,必须是http或https开头',
},
]}
/>
<ProFormSwitch
label={'高级设置'}
checkedChildren='开启'
unCheckedChildren='关闭'
name='customer_link_enable'
tooltip={'开启后可以自定义链接所有信息'}
/>
<ProFormDependency name={['customer_link_enable']}>
{({customer_link_enable}) => {
if (customer_link_enable) {
return (
<>
<ProFormText
name='title'
label='链接标题'
width='md'
rules={[
{
required: true,
message: '请输入链接标题',
},
]}
/>
<ProFormTextArea
name='desc'
label='链接描述'
width='md'
/>
<Form.Item
label='链接封面'
name='picurl'
rules={[
{
required: true,
message: '请上传链接图片!',
},
]}
>
<ImageUploader
customRequest={async (req) => {
await UploadFileFn(req, linkModalFormRef, 'picurl')
}}
/>
</Form.Item>
</>
);
}
return <></>;
}}
</ProFormDependency>
</Spin>
</ModalForm>
<ModalForm
formRef={miniAppModalFormRef}
className={'dialog from-item-label-100w'}
layout={'horizontal'}
width={'560px'}
labelCol={{
md: 6,
}}
visible={currentMode === 'miniprogram' && modalVisible}
onVisibleChange={setModalVisible}
onFinish={async (params) => {
attachments[currentIndex] = {
id: new Date().getTime().toString(),
msgtype: params.msgtype,
name: params.title,
// @ts-ignore
miniprogram: {...params},
};
setAttachments(attachments);
return true;
}}
>
<h2 className='dialog-title'> 添加小程序附件 </h2>
<Alert
showIcon={true}
type='info'
message={
'请填写企业微信后台绑定的小程序id和路径,否则会造成发送失败'
}
style={{marginBottom: 20}}
/>
<ProForm.Item initialValue={'miniprogram'} name={'msgtype'} noStyle={true}>
<input type={'hidden'}/>
</ProForm.Item>
<ProFormText
name='title'
label='小程序标题'
width='md'
rules={[
{
required: true,
message: '请输入链接标题',
},
]}
/>
<ProFormText
// 帮助指引
name='app_id'
label='小程序AppID'
width='md'
rules={[
{
required: true,
message: '请输入小程序AppID',
},
]}
/>
<ProFormText
name='page'
label='小程序路径'
width='md'
rules={[
{
required: true,
message: '请输入小程序路径',
},
]}
/>
<Form.Item
label='小程序封面'
name='pic_media_id'
rules={[
{
required: true,
message: '请小程序封面!',
},
]}
>
<ImageUploader
customRequest={async (req) => {
await UploadFileFn(req, miniAppModalFormRef, 'pic_media_id')
}}
/>
</Form.Item>
</ModalForm>
</>
);
}
Example #21
Source File: index.tsx From fe-v5 with Apache License 2.0 | 4 votes |
function index(props: IProps) {
const { dashboardId, id, time, refreshFlag, step, type, variableConfig, values, isPreview, onCloneClick, onShareClick, onEditClick, onDeleteClick } = props;
const ref = useRef<HTMLDivElement>(null);
const [inViewPort] = useInViewport(ref);
const { series, loading } = usePrometheus({
id,
dashboardId,
time,
refreshFlag,
step,
targets: values.targets,
variableConfig,
inViewPort: isPreview || inViewPort,
});
const subProps = {
values,
series,
};
const tipsVisible = values.description || !_.isEmpty(values.links);
if (_.isEmpty(values)) return null;
const RendererCptMap = {
timeseries: () => <Timeseries {...subProps} />,
stat: () => <Stat {...subProps} />,
table: () => <Table {...subProps} />,
pie: () => <Pie {...subProps} />,
};
return (
<div className='renderer-container' ref={ref}>
<div className='renderer-header graph-header dashboards-panels-item-drag-handle'>
{tipsVisible ? (
<Tooltip
placement='rightTop'
overlayInnerStyle={{
width: 300,
}}
title={
<div>
<Markdown content={values.description} />
<div>
{_.map(values.links, (link) => {
return (
<div style={{ marginTop: 8 }}>
<a href={link.url} target={link.targetBlank ? '_blank' : '_self'}>
{link.title}
</a>
</div>
);
})}
</div>
</div>
}
>
<div className='renderer-header-desc'>
<span className='renderer-header-info-corner-inner' />
{values.description ? <InfoOutlined /> : <LinkOutlined />}
</div>
</Tooltip>
) : null}
<div className='renderer-header-content'>
{!isPreview ? (
<Dropdown
trigger={['click']}
placement='bottomCenter'
overlayStyle={{
minWidth: '100px',
}}
overlay={
<Menu>
{!isPreview ? (
<>
<Menu.Item onClick={onEditClick} key='0'>
<SettingOutlined style={{ marginRight: 8 }} />
编辑
</Menu.Item>
<Menu.Item onClick={onCloneClick} key='1'>
<CopyOutlined style={{ marginRight: 8 }} />
克隆
</Menu.Item>
<Menu.Item onClick={onShareClick} key='2'>
<ShareAltOutlined style={{ marginRight: 8 }} />
分享
</Menu.Item>
<Menu.Item onClick={onDeleteClick} key='3'>
<DeleteOutlined style={{ marginRight: 8 }} />
删除
</Menu.Item>
</>
) : null}
</Menu>
}
>
<div className='renderer-header-title'>
{values.name}
<DownOutlined className='renderer-header-arrow' />
</div>
</Dropdown>
) : (
<div className='renderer-header-title'>{values.name}</div>
)}
</div>
<div className='renderer-header-loading'>{loading && <SyncOutlined spin />}</div>
</div>
<div className='renderer-body' style={{ height: `calc(100% - 36px)` }}>
{RendererCptMap[type] ? RendererCptMap[type]() : `无效的图表类型 ${type}`}
</div>
</div>
);
}
Example #22
Source File: index.tsx From ql with MIT License | 4 votes |
Config = () => {
const columns = [
{
title: '序号',
align: 'center' as const,
render: (text: string, record: any, index: number) => {
return <span style={{ cursor: 'text' }}>{index + 1} </span>;
},
},
{
title: '昵称',
dataIndex: 'nickname',
key: 'nickname',
align: 'center' as const,
width: '15%',
render: (text: string, record: any, index: number) => {
const match = record.value.match(/pt_pin=([^; ]+)(?=;?)/);
const val = (match && match[1]) || '未匹配用户名';
return (
<span style={{ cursor: 'text' }}>{record.nickname || val} </span>
);
},
},
{
title: '值',
dataIndex: 'value',
key: 'value',
align: 'center' as const,
width: '50%',
render: (text: string, record: any) => {
return (
<span
style={{
textAlign: 'left',
display: 'inline-block',
wordBreak: 'break-all',
cursor: 'text',
}}
>
{text}
</span>
);
},
},
{
title: '状态',
key: 'status',
dataIndex: 'status',
align: 'center' as const,
width: '15%',
render: (text: string, record: any, index: number) => {
return (
<Space size="middle" style={{ cursor: 'text' }}>
<Tag
color={StatusColor[record.status] || StatusColor[3]}
style={{ marginRight: 0 }}
>
{Status[record.status]}
</Tag>
{record.status !== Status.已禁用 && (
<Tooltip title="刷新">
<a onClick={() => refreshStatus(record, index)}>
<SyncOutlined />
</a>
</Tooltip>
)}
</Space>
);
},
},
{
title: '操作',
key: 'action',
align: 'center' as const,
render: (text: string, record: any, index: number) => (
<Space size="middle">
<Tooltip title="编辑">
<a onClick={() => editCookie(record, index)}>
<EditOutlined />
</a>
</Tooltip>
<Tooltip title={record.status === Status.已禁用 ? '启用' : '禁用'}>
<a onClick={() => enabledOrDisabledCookie(record, index)}>
{record.status === Status.已禁用 ? (
<CheckCircleOutlined />
) : (
<StopOutlined />
)}
</a>
</Tooltip>
<Tooltip title="删除">
<a onClick={() => deleteCookie(record, index)}>
<DeleteOutlined />
</a>
</Tooltip>
</Space>
),
},
];
const [width, setWidth] = useState('100%');
const [marginLeft, setMarginLeft] = useState(0);
const [marginTop, setMarginTop] = useState(-72);
const [value, setValue] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [isModalVisible, setIsModalVisible] = useState(false);
const [editedCookie, setEditedCookie] = useState();
const [selectedRowIds, setSelectedRowIds] = useState<string[]>([]);
const getCookies = () => {
setLoading(true);
request
.get(`${config.apiPrefix}cookies`)
.then((data: any) => {
setValue(data.data);
})
.finally(() => setLoading(false));
};
const refreshStatus = (record: any, index: number) => {
request
.get(`${config.apiPrefix}cookies/${record._id}/refresh`)
.then(async (data: any) => {
if (data.data && data.data.value) {
(value as any).splice(index, 1, data.data);
setValue([...(value as any)] as any);
} else {
message.error('更新状态失败');
}
});
};
const enabledOrDisabledCookie = (record: any, index: number) => {
Modal.confirm({
title: `确认${record.status === Status.已禁用 ? '启用' : '禁用'}`,
content: (
<>
确认{record.status === Status.已禁用 ? '启用' : '禁用'}
Cookie{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning">
{record.value}
</Text>{' '}
吗
</>
),
onOk() {
request
.put(
`${config.apiPrefix}cookies/${
record.status === Status.已禁用 ? 'enable' : 'disable'
}`,
{
data: [record._id],
},
)
.then((data: any) => {
if (data.code === 200) {
message.success(
`${record.status === Status.已禁用 ? '启用' : '禁用'}成功`,
);
const newStatus =
record.status === Status.已禁用 ? Status.未获取 : Status.已禁用;
const result = [...value];
result.splice(index, 1, {
...record,
status: newStatus,
});
setValue(result);
} else {
message.error(data);
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const addCookie = () => {
setEditedCookie(null as any);
setIsModalVisible(true);
};
const editCookie = (record: any, index: number) => {
setEditedCookie(record);
setIsModalVisible(true);
};
const deleteCookie = (record: any, index: number) => {
Modal.confirm({
title: '确认删除',
content: (
<>
确认删除Cookie{' '}
<Text style={{ wordBreak: 'break-all' }} type="warning">
{record.value}
</Text>{' '}
吗
</>
),
onOk() {
request
.delete(`${config.apiPrefix}cookies`, { data: [record._id] })
.then((data: any) => {
if (data.code === 200) {
message.success('删除成功');
const result = [...value];
result.splice(index, 1);
setValue(result);
} else {
message.error(data);
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const handleCancel = (cookies?: any[]) => {
setIsModalVisible(false);
if (cookies && cookies.length > 0) {
handleCookies(cookies);
}
};
const handleCookies = (cookies: any[]) => {
const result = [...value];
for (let i = 0; i < cookies.length; i++) {
const cookie = cookies[i];
const index = value.findIndex((x) => x._id === cookie._id);
if (index === -1) {
result.push(cookie);
} else {
result.splice(index, 1, {
...cookie,
});
}
}
setValue(result);
};
const components = {
body: {
row: DragableBodyRow,
},
};
const moveRow = useCallback(
(dragIndex, hoverIndex) => {
if (dragIndex === hoverIndex) {
return;
}
const dragRow = value[dragIndex];
const newData = [...value];
newData.splice(dragIndex, 1);
newData.splice(hoverIndex, 0, dragRow);
setValue([...newData]);
request
.put(`${config.apiPrefix}cookies/${dragRow._id}/move`, {
data: { fromIndex: dragIndex, toIndex: hoverIndex },
})
.then((data: any) => {
if (data.code !== 200) {
message.error(data);
}
});
},
[value],
);
const onSelectChange = (selectedIds: any[]) => {
setSelectedRowIds(selectedIds);
};
const rowSelection = {
selectedRowIds,
onChange: onSelectChange,
};
const delCookies = () => {
Modal.confirm({
title: '确认删除',
content: <>确认删除选中的Cookie吗</>,
onOk() {
request
.delete(`${config.apiPrefix}cookies`, { data: selectedRowIds })
.then((data: any) => {
if (data.code === 200) {
message.success('批量删除成功');
setSelectedRowIds([]);
getCookies();
} else {
message.error(data);
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
const operateCookies = (operationStatus: number) => {
Modal.confirm({
title: `确认${OperationName[operationStatus]}`,
content: <>确认{OperationName[operationStatus]}选中的Cookie吗</>,
onOk() {
request
.put(`${config.apiPrefix}cookies/${OperationPath[operationStatus]}`, {
data: selectedRowIds,
})
.then((data: any) => {
if (data.code === 200) {
getCookies();
} else {
message.error(data);
}
});
},
onCancel() {
console.log('Cancel');
},
});
};
useEffect(() => {
if (document.body.clientWidth < 768) {
setWidth('auto');
setMarginLeft(0);
setMarginTop(0);
} else {
setWidth('100%');
setMarginLeft(0);
setMarginTop(-72);
}
getCookies();
}, []);
return (
<PageContainer
className="session-wrapper"
title="Session管理"
extra={[
<Button key="2" type="primary" onClick={() => addCookie()}>
添加Cookie
</Button>,
]}
header={{
style: {
padding: '4px 16px 4px 15px',
position: 'sticky',
top: 0,
left: 0,
zIndex: 20,
marginTop,
width,
marginLeft,
},
}}
>
{selectedRowIds.length > 0 && (
<div style={{ marginBottom: 16 }}>
<Button
type="primary"
style={{ marginBottom: 5 }}
onClick={delCookies}
>
批量删除
</Button>
<Button
type="primary"
onClick={() => operateCookies(0)}
style={{ marginLeft: 8, marginBottom: 5 }}
>
批量启用
</Button>
<Button
type="primary"
onClick={() => operateCookies(1)}
style={{ marginLeft: 8, marginRight: 8 }}
>
批量禁用
</Button>
<span style={{ marginLeft: 8 }}>
已选择
<a>{selectedRowIds?.length}</a>项
</span>
</div>
)}
<DndProvider backend={HTML5Backend}>
<Table
columns={columns}
rowSelection={rowSelection}
pagination={false}
dataSource={value}
rowKey="_id"
size="middle"
scroll={{ x: 768 }}
components={components}
loading={loading}
onRow={(record, index) => {
return {
index,
moveRow,
} as any;
}}
/>
</DndProvider>
<CookieModal
visible={isModalVisible}
handleCancel={handleCancel}
cookie={editedCookie}
/>
</PageContainer>
);
}
Example #23
Source File: Graph.tsx From fe-v5 with Apache License 2.0 | 4 votes |
export default function Graph(props: IProps) {
const { metric, match, range, step, onClose } = props;
const newGroups = _.map(
_.filter(match.dimensionLabels, (item) => !_.isEmpty(item.value)),
'label',
);
const [refreshFlag, setRefreshFlag] = useState(_.uniqueId('refreshFlag_'));
const [calcFunc, setCalcFunc] = useState('');
const [comparison, setComparison] = useState<string[]>([]);
const [aggrFunc, setAggrFunc] = useState('avg');
const [aggrGroups, setAggrGroups] = useState<string[]>(newGroups);
const [labels, setLabels] = useState<string[]>([]);
const [series, setSeries] = useState<any[]>([]);
const [highLevelConfig, setHighLevelConfig] = useState({
shared: true,
sharedSortDirection: 'desc',
legend: true,
util: 'none',
colorRange: colors[0].value,
reverseColorOrder: false,
colorDomainAuto: true,
colorDomain: [],
chartheight: 300,
});
const [chartType, setChartType] = useState('line');
const [reduceFunc, setReduceFunc] = useState('last');
const lineGraphProps = {
custom: {
drawStyle: 'lines',
fillOpacity: 0,
stack: 'hidden',
lineInterpolation: 'smooth',
},
options: {
legend: {
displayMode: highLevelConfig.legend ? 'list' : 'hidden',
},
tooltip: {
mode: highLevelConfig.shared ? 'all' : 'single',
sort: highLevelConfig.sharedSortDirection,
},
standardOptions: {
util: highLevelConfig.util,
},
},
};
const hexbinGraphProps = {
custom: {
calc: reduceFunc,
colorRange: highLevelConfig.colorRange,
reverseColorOrder: highLevelConfig.reverseColorOrder,
colorDomainAuto: highLevelConfig.colorDomainAuto,
colorDomain: highLevelConfig.colorDomain,
},
options: {
standardOptions: {
util: highLevelConfig.util,
},
},
};
const graphStandardOptions = {
line: <LineGraphStandardOptions highLevelConfig={highLevelConfig} setHighLevelConfig={setHighLevelConfig} />,
hexbin: <HexbinGraphStandardOptions highLevelConfig={highLevelConfig} setHighLevelConfig={setHighLevelConfig} />,
};
useEffect(() => {
setAggrGroups(newGroups);
}, [JSON.stringify(newGroups)]);
useEffect(() => {
const matchStr = getMatchStr(match);
getLabels(`${metric}${matchStr}`, range).then((res) => {
setLabels(res);
});
}, [refreshFlag, JSON.stringify(match), JSON.stringify(range)]);
useEffect(() => {
getQueryRange({
metric,
match: getMatchStr(match),
range,
step,
aggrFunc,
aggrGroups,
calcFunc,
comparison,
}).then((res) => {
setSeries(res);
});
}, [refreshFlag, metric, JSON.stringify(match), JSON.stringify(range), step, calcFunc, comparison, aggrFunc, aggrGroups]);
return (
<Card
size='small'
style={{ marginBottom: 10 }}
title={metric}
className='n9e-metric-views-metrics-graph'
extra={
<Space>
<Space size={0} style={{ marginRight: 10 }}>
<LineChartOutlined
className={classNames({
'button-link-icon': true,
active: chartType === 'line',
})}
onClick={() => {
setChartType('line');
}}
/>
<Divider type='vertical' />
<HexbinIcon
className={classNames({
'button-link-icon': true,
active: chartType === 'hexbin',
})}
onClick={() => {
setChartType('hexbin');
}}
/>
</Space>
<Popover placement='left' content={graphStandardOptions[chartType]} trigger='click' autoAdjustOverflow={false} getPopupContainer={() => document.body}>
<a>
<SettingOutlined />
</a>
</Popover>
<a>
<SyncOutlined
onClick={() => {
setRefreshFlag(_.uniqueId('refreshFlag_'));
}}
/>
</a>
<a>
<ShareAltOutlined
onClick={() => {
const curCluster = localStorage.getItem('curCluster');
const dataProps = {
type: 'timeseries',
version: '2.0.0',
name: metric,
step,
range,
...lineGraphProps,
targets: _.map(
getExprs({
metric,
match: getMatchStr(match),
aggrFunc,
aggrGroups,
calcFunc,
comparison,
}),
(expr) => {
return {
expr,
};
},
),
};
setTmpChartData([
{
configs: JSON.stringify({
curCluster,
dataProps,
}),
},
]).then((res) => {
const ids = res.dat;
window.open('/chart/' + ids);
});
}}
/>
</a>
<a>
<CloseCircleOutlined onClick={onClose} />
</a>
</Space>
}
>
<div>
<Space>
<div>
计算函数:
<Dropdown
overlay={
<Menu onClick={(e) => setCalcFunc(e.key === 'clear' ? '' : e.key)} selectedKeys={[calcFunc]}>
<Menu.Item key='rate_1m'>rate_1m</Menu.Item>
<Menu.Item key='rate_5m'>rate_5m</Menu.Item>
<Menu.Item key='increase_1m'>increase_1m</Menu.Item>
<Menu.Item key='increase_5m'>increase_5m</Menu.Item>
<Menu.Divider></Menu.Divider>
<Menu.Item key='clear'>clear</Menu.Item>
</Menu>
}
>
<a className='ant-dropdown-link' onClick={(e) => e.preventDefault()}>
{calcFunc || '无'} <DownOutlined />
</a>
</Dropdown>
</div>
<div>
环比:
{comparison.map((ag) => (
<Tag
key={ag}
closable
onClose={() => {
setComparison(_.without(comparison, ag));
}}
>
{ag}
</Tag>
))}
<Dropdown
overlay={
<Menu
onClick={(e) => {
if (comparison.indexOf(e.key) === -1) {
setComparison([...comparison, e.key]);
} else {
setComparison(_.without(comparison, e.key));
}
}}
selectedKeys={comparison}
>
<Menu.Item key='1d'>1d</Menu.Item>
<Menu.Item key='7d'>7d</Menu.Item>
</Menu>
}
overlayStyle={{ maxHeight: 400, overflow: 'auto' }}
>
<a className='ant-dropdown-link' onClick={(e) => e.preventDefault()}>
<PlusCircleOutlined />
</a>
</Dropdown>
</div>
<div>
聚合函数:
<Dropdown
overlay={
<Menu onClick={(e) => setAggrFunc(e.key)} selectedKeys={[aggrFunc]}>
<Menu.Item key='sum'>sum</Menu.Item>
<Menu.Item key='avg'>avg</Menu.Item>
<Menu.Item key='max'>max</Menu.Item>
<Menu.Item key='min'>min</Menu.Item>
</Menu>
}
>
<a className='ant-dropdown-link' onClick={(e) => e.preventDefault()}>
{aggrFunc} <DownOutlined />
</a>
</Dropdown>
</div>
{aggrFunc ? (
<div className='graph-config-inner-item'>
聚合维度:
{aggrGroups.map((ag) => (
<Tag
key={ag}
closable
onClose={() => {
setAggrGroups(_.without(aggrGroups, ag));
}}
>
{ag}
</Tag>
))}
<Dropdown
overlay={
<Menu
onClick={(e) => {
if (aggrGroups.indexOf(e.key) === -1) {
setAggrGroups([...aggrGroups, e.key]);
} else {
setAggrGroups(_.without(aggrGroups, e.key));
}
}}
selectedKeys={aggrGroups}
>
{_.map(
_.filter(labels, (n) => n !== '__name__'),
(ag) => (
<Menu.Item key={ag}>{ag}</Menu.Item>
),
)}
</Menu>
}
overlayStyle={{ maxHeight: 400, overflow: 'auto' }}
>
<a className='ant-dropdown-link' onClick={(e) => e.preventDefault()}>
<PlusCircleOutlined />
</a>
</Dropdown>
</div>
) : null}
{chartType === 'hexbin' && (
<div>
取值计算:
<Dropdown
overlay={
<Menu onClick={(e) => setReduceFunc(e.key)} selectedKeys={[reduceFunc]}>
{_.map(calcsOptions, (val, key) => {
return <Menu.Item key={key}>{val.name}</Menu.Item>;
})}
</Menu>
}
>
<a className='ant-dropdown-link' onClick={(e) => e.preventDefault()}>
{calcsOptions[reduceFunc]?.name} <DownOutlined />
</a>
</Dropdown>
</div>
)}
</Space>
</div>
<div>
{chartType === 'line' && <Timeseries inDashboard={false} values={lineGraphProps as any} series={series} />}
{chartType === 'hexbin' && (
<div style={{ padding: '20px 0 0 0', height: highLevelConfig.chartheight }}>
<Hexbin values={hexbinGraphProps as any} series={series} />
</div>
)}
</div>
</Card>
);
}
Example #24
Source File: index.tsx From fe-v5 with Apache License 2.0 | 4 votes |
function index(props: IProps) {
const { dashboardId, id, time, refreshFlag, step, type, variableConfig, isPreview, onCloneClick, onShareClick, onEditClick, onDeleteClick } = props;
const values = _.cloneDeep(props.values);
const ref = useRef<HTMLDivElement>(null);
const [inViewPort] = useInViewport(ref);
const { series, loading } = usePrometheus({
id,
dashboardId,
time,
refreshFlag,
step,
targets: values.targets,
variableConfig,
inViewPort: isPreview || inViewPort,
});
const tipsVisible = values.description || !_.isEmpty(values.links);
if (_.isEmpty(values)) return null;
// TODO: 如果 hexbin 的 colorRange 为 string 时转成成 array
if (typeof _.get(values, 'custom.colorRange') === 'string') {
_.set(values, 'custom.colorRange', _.split(_.get(values, 'custom.colorRange'), ','));
}
const subProps = {
values,
series,
};
const RendererCptMap = {
timeseries: () => <Timeseries {...subProps} />,
stat: () => <Stat {...subProps} />,
table: () => <Table {...subProps} />,
pie: () => <Pie {...subProps} />,
hexbin: () => <Hexbin {...subProps} />,
};
return (
<div className='renderer-container' ref={ref}>
<div className='renderer-header graph-header dashboards-panels-item-drag-handle'>
{tipsVisible ? (
<Tooltip
placement='rightTop'
overlayInnerStyle={{
width: 300,
}}
title={
<div>
<Markdown content={values.description} />
<div>
{_.map(values.links, (link, i) => {
return (
<div key={i} style={{ marginTop: 8 }}>
<a href={link.url} target={link.targetBlank ? '_blank' : '_self'}>
{link.title}
</a>
</div>
);
})}
</div>
</div>
}
>
<div className='renderer-header-desc'>
<span className='renderer-header-info-corner-inner' />
{values.description ? <InfoOutlined /> : <LinkOutlined />}
</div>
</Tooltip>
) : null}
<div className='renderer-header-content'>
{!isPreview ? (
<Dropdown
trigger={['click']}
placement='bottomCenter'
overlayStyle={{
minWidth: '100px',
}}
overlay={
<Menu>
{!isPreview ? (
<>
<Menu.Item onClick={onEditClick} key='0'>
<SettingOutlined style={{ marginRight: 8 }} />
编辑
</Menu.Item>
<Menu.Item onClick={onCloneClick} key='1'>
<CopyOutlined style={{ marginRight: 8 }} />
克隆
</Menu.Item>
<Menu.Item onClick={onShareClick} key='2'>
<ShareAltOutlined style={{ marginRight: 8 }} />
分享
</Menu.Item>
<Menu.Item onClick={onDeleteClick} key='3'>
<DeleteOutlined style={{ marginRight: 8 }} />
删除
</Menu.Item>
</>
) : null}
</Menu>
}
>
<div className='renderer-header-title'>
{values.name}
<DownOutlined className='renderer-header-arrow' />
</div>
</Dropdown>
) : (
<div className='renderer-header-title'>{values.name}</div>
)}
</div>
<div className='renderer-header-loading'>{loading && <SyncOutlined spin />}</div>
</div>
<div className='renderer-body' style={{ height: `calc(100% - 36px)` }}>
{RendererCptMap[type] ? RendererCptMap[type]() : `无效的图表类型 ${type}`}
</div>
</div>
);
}
Example #25
Source File: palette.tsx From jmix-frontend with Apache License 2.0 | 4 votes |
palette = () => (
<Palette>
<Category name="Text">
<Component name="Formatted Message">
<Variant>
<FormattedMessage />
</Variant>
</Component>
<Component name="Heading">
<Variant name="h1">
<Typography.Title></Typography.Title>
</Variant>
<Variant name="h2">
<Typography.Title level={2}></Typography.Title>
</Variant>
<Variant name="h3">
<Typography.Title level={3}></Typography.Title>
</Variant>
<Variant name="h4">
<Typography.Title level={4}></Typography.Title>
</Variant>
<Variant name="h5">
<Typography.Title level={5}></Typography.Title>
</Variant>
</Component>
<Component name="Text">
<Variant>
<Typography.Text></Typography.Text>
</Variant>
<Variant name="Secondary">
<Typography.Text type="secondary"></Typography.Text>
</Variant>
<Variant name="Success">
<Typography.Text type="success"></Typography.Text>
</Variant>
<Variant name="Warning">
<Typography.Text type="warning"></Typography.Text>
</Variant>
<Variant name="Danger">
<Typography.Text type="danger"></Typography.Text>
</Variant>
<Variant name="Disabled">
<Typography.Text disabled></Typography.Text>
</Variant>
</Component>
</Category>
<Category name="Layout">
<Component name="Divider">
<Variant>
<Divider />
</Variant>
</Component>
<Component name="Grid">
<Variant name="Simple Row">
<Row></Row>
</Variant>
<Variant name="Two columns">
<Row>
<Col span={12}></Col>
<Col span={12}></Col>
</Row>
</Variant>
<Variant name="Three columns">
<Row>
<Col span={8}></Col>
<Col span={8}></Col>
<Col span={8}></Col>
</Row>
</Variant>
</Component>
<Component name="Space">
<Variant>
<Space />
</Variant>
<Variant name="Small">
<Space size={"small"} />
</Variant>
<Variant name="Large">
<Space size={"large"} />
</Variant>
</Component>
</Category>
<Category name="Controls">
<Component name="Autocomplete">
<Variant>
<AutoComplete placeholder="input here" />
</Variant>
</Component>
<Component name="Button">
<Variant>
<Button></Button>
</Variant>
<Variant name="Primary">
<Button type="primary"></Button>
</Variant>
<Variant name="Link">
<Button type="link"></Button>
</Variant>
<Variant name="Dropdown">
<Dropdown
trigger={["click"]}
overlay={
<Menu>
<Menu.Item></Menu.Item>
<Menu.Item></Menu.Item>
<Menu.Item></Menu.Item>
</Menu>
}
>
<Button></Button>
</Dropdown>
</Variant>
</Component>
<Component name="Checkbox">
<Variant>
<Checkbox />
</Variant>
</Component>
<Component name="Switch">
<Variant>
<Switch />
</Variant>
</Component>
<Component name="Radio Group">
<Variant>
<Radio.Group>
<Radio value={1}>A</Radio>
<Radio value={2}>B</Radio>
<Radio value={3}>C</Radio>
<Radio value={4}>D</Radio>
</Radio.Group>
</Variant>
<Variant name="Button">
<Radio.Group>
<Radio.Button value={1}>A</Radio.Button>
<Radio.Button value={2}>B</Radio.Button>
<Radio.Button value={3}>C</Radio.Button>
<Radio.Button value={4}>D</Radio.Button>
</Radio.Group>
</Variant>
</Component>
<Component name="DatePicker">
<Variant>
<DatePicker />
</Variant>
<Variant name="Range">
<DatePicker.RangePicker />
</Variant>
</Component>
<Component name="TimePicker">
<Variant>
<TimePicker />
</Variant>
<Variant name="Range">
<TimePicker.RangePicker />
</Variant>
</Component>
<Component name="Input">
<Variant>
<Input />
</Variant>
<Variant name="Number">
<InputNumber />
</Variant>
</Component>
<Component name="Select">
<Variant>
<Select defaultValue="1">
<Select.Option value="1">1</Select.Option>
<Select.Option value="2">2</Select.Option>
</Select>
</Variant>
<Variant name="Multiple">
<Select defaultValue={["1"]} mode="multiple" allowClear>
<Select.Option value="1">1</Select.Option>
<Select.Option value="2">2</Select.Option>
</Select>
</Variant>
</Component>
<Component name="Link">
<Variant>
<Typography.Link href="" target="_blank"></Typography.Link>
</Variant>
</Component>
<Component name="Slider">
<Variant>
<Slider defaultValue={30} />
</Variant>
<Variant name="Range">
<Slider range defaultValue={[20, 50]} />
</Variant>
</Component>
</Category>
<Category name="Data Display">
<Component name="Field">
<Variant>
<Field
entityName={ENTITY_NAME}
disabled={readOnlyMode}
propertyName=""
formItemProps={{
style: { marginBottom: "12px" }
}}
/>
</Variant>
</Component>
<Component name="Card">
<Variant>
<Card />
</Variant>
<Variant name="With Title">
<Card>
<Card title="Card title">
<p>Card content</p>
</Card>
</Card>
</Variant>
<Variant name="My custom card">
<Card>
<Card title="Card title">
<p>Card content</p>
<Avatar />
</Card>
</Card>
</Variant>
</Component>
<Component name="Tabs">
<Variant>
<Tabs defaultActiveKey="1">
<Tabs.TabPane tab="Tab 1" key="1">
Content of Tab Pane 1
</Tabs.TabPane>
<Tabs.TabPane tab="Tab 2" key="2">
Content of Tab Pane 2
</Tabs.TabPane>
<Tabs.TabPane tab="Tab 3" key="3">
Content of Tab Pane 3
</Tabs.TabPane>
</Tabs>
</Variant>
<Variant name="Tab Pane">
<Tabs.TabPane></Tabs.TabPane>
</Variant>
</Component>
<Component name="Collapse">
<Variant>
<Collapse defaultActiveKey="1">
<Collapse.Panel
header="This is panel header 1"
key="1"
></Collapse.Panel>
<Collapse.Panel
header="This is panel header 2"
key="2"
></Collapse.Panel>
<Collapse.Panel
header="This is panel header 3"
key="3"
></Collapse.Panel>
</Collapse>
</Variant>
</Component>
<Component name="Image">
<Variant>
<Image width={200} src="" />
</Variant>
</Component>
<Component name="Avatar">
<Variant>
<Avatar icon={<UserOutlined />} />
</Variant>
<Variant name="Image">
<Avatar src="https://joeschmoe.io/api/v1/random" />
</Variant>
</Component>
<Component name="Badge">
<Variant>
<Badge count={1}></Badge>
</Variant>
</Component>
<Component name="Statistic">
<Variant>
<Statistic title="Title" value={112893} />
</Variant>
</Component>
<Component name="Alert">
<Variant name="Success">
<Alert message="Text" type="success" />
</Variant>
<Variant name="Info">
<Alert message="Text" type="info" />
</Variant>
<Variant name="Warning">
<Alert message="Text" type="warning" />
</Variant>
<Variant name="Error">
<Alert message="Text" type="error" />
</Variant>
</Component>
<Component name="List">
<Variant>
<List
bordered
dataSource={[]}
renderItem={item => <List.Item></List.Item>}
/>
</Variant>
</Component>
</Category>
<Category name="Icons">
<Component name="Arrow">
<Variant name="Up">
<ArrowUpOutlined />
</Variant>
<Variant name="Down">
<ArrowDownOutlined />
</Variant>
<Variant name="Left">
<ArrowLeftOutlined />
</Variant>
<Variant name="Right">
<ArrowRightOutlined />
</Variant>
</Component>
<Component name="Question">
<Variant>
<QuestionOutlined />
</Variant>
<Variant name="Circle">
<QuestionCircleOutlined />
</Variant>
</Component>
<Component name="Plus">
<Variant>
<PlusOutlined />
</Variant>
<Variant name="Circle">
<PlusCircleOutlined />
</Variant>
</Component>
<Component name="Info">
<Variant>
<InfoOutlined />
</Variant>
<Variant name="Circle">
<InfoCircleOutlined />
</Variant>
</Component>
<Component name="Exclamation">
<Variant>
<ExclamationOutlined />
</Variant>
<Variant name="Circle">
<ExclamationCircleOutlined />
</Variant>
</Component>
<Component name="Close">
<Variant>
<CloseOutlined />
</Variant>
<Variant name="Circle">
<CloseCircleOutlined />
</Variant>
</Component>
<Component name="Check">
<Variant>
<CheckOutlined />
</Variant>
<Variant name="Circle">
<CheckCircleOutlined />
</Variant>
</Component>
<Component name="Edit">
<Variant>
<EditOutlined />
</Variant>
</Component>
<Component name="Copy">
<Variant>
<CopyOutlined />
</Variant>
</Component>
<Component name="Delete">
<Variant>
<DeleteOutlined />
</Variant>
</Component>
<Component name="Bars">
<Variant>
<BarsOutlined />
</Variant>
</Component>
<Component name="Bell">
<Variant>
<BellOutlined />
</Variant>
</Component>
<Component name="Clear">
<Variant>
<ClearOutlined />
</Variant>
</Component>
<Component name="Download">
<Variant>
<DownloadOutlined />
</Variant>
</Component>
<Component name="Upload">
<Variant>
<UploadOutlined />
</Variant>
</Component>
<Component name="Sync">
<Variant>
<SyncOutlined />
</Variant>
</Component>
<Component name="Save">
<Variant>
<SaveOutlined />
</Variant>
</Component>
<Component name="Search">
<Variant>
<SearchOutlined />
</Variant>
</Component>
<Component name="Settings">
<Variant>
<SettingOutlined />
</Variant>
</Component>
<Component name="Paperclip">
<Variant>
<PaperClipOutlined />
</Variant>
</Component>
<Component name="Phone">
<Variant>
<PhoneOutlined />
</Variant>
</Component>
<Component name="Mail">
<Variant>
<MailOutlined />
</Variant>
</Component>
<Component name="Home">
<Variant>
<HomeOutlined />
</Variant>
</Component>
<Component name="Contacts">
<Variant>
<ContactsOutlined />
</Variant>
</Component>
<Component name="User">
<Variant>
<UserOutlined />
</Variant>
<Variant name="Add">
<UserAddOutlined />
</Variant>
<Variant name="Remove">
<UserDeleteOutlined />
</Variant>
</Component>
<Component name="Team">
<Variant>
<TeamOutlined />
</Variant>
</Component>
</Category>
<Category name="Screens">
<Component name="ExampleCustomScreen">
<Variant>
<ExampleCustomScreen />
</Variant>
</Component>
<Component name="CustomEntityFilterTest">
<Variant>
<CustomEntityFilterTest />
</Variant>
</Component>
<Component name="CustomFormControls">
<Variant>
<CustomFormControls />
</Variant>
</Component>
<Component name="CustomDataDisplayComponents">
<Variant>
<CustomDataDisplayComponents />
</Variant>
</Component>
<Component name="CustomAppLayouts">
<Variant>
<CustomAppLayouts />
</Variant>
</Component>
<Component name="CustomControls">
<Variant>
<CustomControls />
</Variant>
</Component>
<Component name="ErrorBoundaryTests">
<Variant>
<ErrorBoundaryTests />
</Variant>
</Component>
<Component name="TestBlankScreen">
<Variant>
<TestBlankScreen />
</Variant>
</Component>
<Component name="CarEditor">
<Variant>
<CarEditor />
</Variant>
</Component>
<Component name="CarBrowserCards">
<Variant>
<CarBrowserCards />
</Variant>
</Component>
<Component name="CarBrowserList">
<Variant>
<CarBrowserList />
</Variant>
</Component>
<Component name="CarBrowserTable">
<Variant>
<CarBrowserTable />
</Variant>
</Component>
<Component name="CarCardsGrid">
<Variant>
<CarCardsGrid />
</Variant>
</Component>
<Component name="FavoriteCars">
<Variant>
<FavoriteCars />
</Variant>
</Component>
<Component name="CarCardsWithDetails">
<Variant>
<CarCardsWithDetails />
</Variant>
</Component>
<Component name="CarTableWithFilters">
<Variant>
<CarTableWithFilters />
</Variant>
</Component>
<Component name="CarMasterDetail">
<Variant>
<CarMasterDetail />
</Variant>
</Component>
<Component name="FormWizardCompositionO2O">
<Variant>
<FormWizardCompositionO2O />
</Variant>
</Component>
<Component name="FormWizardEditor">
<Variant>
<FormWizardEditor />
</Variant>
</Component>
<Component name="FormWizardBrowserTable">
<Variant>
<FormWizardBrowserTable />
</Variant>
</Component>
<Component name="CarMultiSelectionTable">
<Variant>
<CarMultiSelectionTable />
</Variant>
</Component>
<Component name="DatatypesTestEditor">
<Variant>
<DatatypesTestEditor />
</Variant>
</Component>
<Component name="DatatypesTestBrowserCards">
<Variant>
<DatatypesTestBrowserCards />
</Variant>
</Component>
<Component name="DatatypesTestBrowserList">
<Variant>
<DatatypesTestBrowserList />
</Variant>
</Component>
<Component name="DatatypesTestBrowserTable">
<Variant>
<DatatypesTestBrowserTable />
</Variant>
</Component>
<Component name="DatatypesTestCards">
<Variant>
<DatatypesTestCards />
</Variant>
</Component>
<Component name="AssociationO2OEditor">
<Variant>
<AssociationO2OEditor />
</Variant>
</Component>
<Component name="AssociationO2OBrowserTable">
<Variant>
<AssociationO2OBrowserTable />
</Variant>
</Component>
<Component name="AssociationO2MEditor">
<Variant>
<AssociationO2MEditor />
</Variant>
</Component>
<Component name="AssociationO2MBrowserTable">
<Variant>
<AssociationO2MBrowserTable />
</Variant>
</Component>
<Component name="AssociationM2OEditor">
<Variant>
<AssociationM2OEditor />
</Variant>
</Component>
<Component name="AssociationM2OBrowserTable">
<Variant>
<AssociationM2OBrowserTable />
</Variant>
</Component>
<Component name="AssociationM2MEditor">
<Variant>
<AssociationM2MEditor />
</Variant>
</Component>
<Component name="AssociationM2MBrowserTable">
<Variant>
<AssociationM2MBrowserTable />
</Variant>
</Component>
<Component name="CompositionO2OEditor">
<Variant>
<CompositionO2OEditor />
</Variant>
</Component>
<Component name="CompositionO2OBrowserTable">
<Variant>
<CompositionO2OBrowserTable />
</Variant>
</Component>
<Component name="CompositionO2MEditor">
<Variant>
<CompositionO2MEditor />
</Variant>
</Component>
<Component name="CompositionO2MBrowserTable">
<Variant>
<CompositionO2MBrowserTable />
</Variant>
</Component>
<Component name="DeeplyNestedTestEntityEditor">
<Variant>
<DeeplyNestedTestEntityEditor />
</Variant>
</Component>
<Component name="DeeplyNestedO2MTestEntityTable">
<Variant>
<DeeplyNestedO2MTestEntityTable />
</Variant>
</Component>
<Component name="DeeplyNestedO2MTestEntityEditor">
<Variant>
<DeeplyNestedO2MTestEntityEditor />
</Variant>
</Component>
<Component name="IntIdEditor">
<Variant>
<IntIdEditor />
</Variant>
</Component>
<Component name="IntIdBrowserTable">
<Variant>
<IntIdBrowserTable />
</Variant>
</Component>
<Component name="IntIdBrowserCards">
<Variant>
<IntIdBrowserCards />
</Variant>
</Component>
<Component name="IntIdBrowserList">
<Variant>
<IntIdBrowserList />
</Variant>
</Component>
<Component name="IntIdentityIdCards">
<Variant>
<IntIdentityIdCards />
</Variant>
</Component>
<Component name="IntIdentityIdEditor">
<Variant>
<IntIdentityIdEditor />
</Variant>
</Component>
<Component name="IntIdentityIdBrowserTable">
<Variant>
<IntIdentityIdBrowserTable />
</Variant>
</Component>
<Component name="IntIdentityIdBrowserCards">
<Variant>
<IntIdentityIdBrowserCards />
</Variant>
</Component>
<Component name="IntIdentityIdBrowserList">
<Variant>
<IntIdentityIdBrowserList />
</Variant>
</Component>
<Component name="StringIdCards">
<Variant>
<StringIdCards />
</Variant>
</Component>
<Component name="StringIdMgtCardsEdit">
<Variant>
<StringIdMgtCardsEdit />
</Variant>
</Component>
<Component name="StringIdBrowserCards">
<Variant>
<StringIdBrowserCards />
</Variant>
</Component>
<Component name="StringIdBrowserList">
<Variant>
<StringIdBrowserList />
</Variant>
</Component>
<Component name="StringIdBrowserTable">
<Variant>
<StringIdBrowserTable />
</Variant>
</Component>
<Component name="WeirdStringIdEditor">
<Variant>
<WeirdStringIdEditor />
</Variant>
</Component>
<Component name="WeirdStringIdBrowserCards">
<Variant>
<WeirdStringIdBrowserCards />
</Variant>
</Component>
<Component name="WeirdStringIdBrowserList">
<Variant>
<WeirdStringIdBrowserList />
</Variant>
</Component>
<Component name="WeirdStringIdBrowserTable">
<Variant>
<WeirdStringIdBrowserTable />
</Variant>
</Component>
<Component name="BoringStringIdEditor">
<Variant>
<BoringStringIdEditor />
</Variant>
</Component>
<Component name="BoringStringIdBrowserTable">
<Variant>
<BoringStringIdBrowserTable />
</Variant>
</Component>
<Component name="TrickyIdEditor">
<Variant>
<TrickyIdEditor />
</Variant>
</Component>
<Component name="TrickyIdBrowserTable">
<Variant>
<TrickyIdBrowserTable />
</Variant>
</Component>
</Category>
</Palette>
)
Example #26
Source File: index.tsx From posthog-foss with MIT License | 4 votes |
export function PreflightCheck(): JSX.Element {
const { preflight, preflightLoading, preflightMode } = useValues(preflightLogic)
const { setPreflightMode } = useActions(preflightLogic)
const isReady =
preflight &&
preflight.django &&
preflight.db &&
preflight.redis &&
preflight.celery &&
(preflightMode === 'experimentation' || preflight.plugins)
const checks = [
{
id: 'database',
name: 'Database (Postgres)',
status: preflight?.db,
},
{
id: 'backend',
name: 'Backend server (Django)',
status: preflight?.django,
},
{
id: 'redis',
name: 'Cache & queue (Redis)',
status: preflight?.redis,
},
{
id: 'celery',
name: 'Background jobs (Celery)',
status: preflight?.celery,
},
{
id: 'plugins',
name: 'Plugin server (Node)',
status: preflight?.plugins,
caption: preflightMode === 'experimentation' ? 'Required in production environments' : '',
failedState: preflightMode === 'experimentation' ? 'warning' : 'error',
},
{
id: 'frontend',
name: 'Frontend build (Webpack)',
status: true, // If this code is ran, the front-end is already built
},
{
id: 'tls',
name: 'SSL/TLS certificate',
status: window.location.protocol === 'https:',
caption:
preflightMode === 'experimentation'
? 'Not required for development or testing'
: 'Install before ingesting real user data',
failedState: preflightMode === 'experimentation' ? 'not-required' : 'warning',
},
] as CheckInterface[]
const handlePreflightFinished = (): void => {
router.actions.push(urls.signup())
}
return (
<div style={{ minHeight: '100vh' }}>
<Row
style={{
display: 'flex',
justifyContent: 'center',
paddingTop: 32,
paddingBottom: 32,
backgroundColor: '#eeefe9',
}}
>
<img src={posthogLogo} style={{ width: 157, height: 30 }} />
</Row>
<Row style={{ display: 'flex', justifyContent: 'center', paddingBottom: 16 }}>
<PageHeader title="Lets get started..." />
</Row>
<Row style={{ display: 'flex', justifyContent: 'center' }}>
<div style={{ width: 960 }}>
<Steps current={0}>
<Step title="Preflight check" subTitle="1 min" description="Prepare your instance" />
<Step
title="Event capture"
subTitle="15 mins"
description="Set up your app to capture events"
/>
<Step
title="Setup your team"
subTitle="5 mins"
description="Invite your team and start discovering insights"
/>
</Steps>
</div>
</Row>
<Row style={{ display: 'flex', justifyContent: 'center' }}>
<div style={{ display: 'flex', alignItems: 'center', flexDirection: 'column' }}>
<img src={suprisedHog} style={{ maxHeight: '100%', width: 320 }} />
<p>Any questions?</p>
<Button type="default" data-attr="support" data-source="preflight">
<a href="https://posthog.com/support" target="_blank" rel="noreferrer">
Get support
</a>
</Button>
</div>
<div
style={{
display: 'flex',
justifyContent: 'flex-start',
margin: '0 32px',
flexDirection: 'column',
paddingTop: 32,
}}
>
<Card style={{ width: '100%' }}>
<Row style={{ display: 'flex', justifyContent: 'space-between', lineHeight: '32px' }}>
{!preflightMode ? (
<b style={{ fontSize: 16 }}>Select launch mode</b>
) : (
<>
<b style={{ fontSize: 16 }}>
<span>
<span
style={{ color: blue.primary, cursor: 'pointer' }}
onClick={() => setPreflightMode(null)}
>
Select launch mode
</span>{' '}
> {capitalizeFirstLetter(preflightMode)}
</span>
</b>
<Button
type="default"
data-attr="preflight-refresh"
icon={<SyncOutlined />}
onClick={() => window.location.reload()}
disabled={preflightLoading || !preflight}
>
Refresh
</Button>
</>
)}
</Row>
{!preflightMode && (
<div>We're excited to have you here. What's your plan for this installation?</div>
)}
<div
className="text-center"
style={{ padding: '24px 0', display: 'flex', justifyContent: 'center', maxWidth: 533 }}
>
{!preflightMode && (
<>
<Button
type="default"
data-attr="preflight-experimentation"
size="large"
onClick={() => setPreflightMode('experimentation')}
icon={<ApiTwoTone />}
>
Just playing
</Button>
<Button
type="primary"
style={{ marginLeft: 16 }}
size="large"
data-attr="preflight-live"
onClick={() => setPreflightMode('live')}
icon={<RocketFilled />}
>
Live implementation
</Button>
</>
)}
{preflightMode && (
<>
<Row>
{checks.map((item) => (
<PreflightItem key={item.id} {...item} />
))}
</Row>
</>
)}
</div>
</Card>
{preflightMode && (
<>
<div className="space-top text-center" data-attr="preflightStatus">
{isReady ? (
<b style={{ color: green.primary }}>All systems go!</b>
) : (
<b>Checks in progress…</b>
)}
</div>
<div className="text-center" style={{ marginBottom: 64 }}>
<Button
type="primary"
data-attr="preflight-complete"
data-source={preflightMode}
disabled={!isReady}
onClick={handlePreflightFinished}
>
Continue
</Button>
</div>
</>
)}
</div>
</Row>
</div>
)
}
Example #27
Source File: workers.tsx From leek with Apache License 2.0 | 4 votes |
WorkersPage = () => {
// STATE
const service = new WorkerService();
const { currentApp, currentEnv } = useApplication();
const [qpHostname, setQPHostname] = useQueryParam("hostname", StringParam);
// Data
const columns = WorkerDataColumns();
const [workers, setWorkers] = useState<any[]>([]);
const [loading, setLoading] = useState<boolean>();
const [currentWorker, setCurrentWorker] = useState({});
const [pagination, setPagination] = useState<any>({
pageSize: 10,
current: 1,
});
// Controls
const [stateFilter, setStateFilter] = useState<string | null>("HEARTBEAT");
// UI
const [workerDetailsVisible, setDetailsVisible] = useState(false);
/** =======================
* Calls
---------------------- **/
function filterWorkers(pager = { current: 1, pageSize: 10 }) {
if (!currentApp) return;
setLoading(true);
let from_ = (pager.current - 1) * pager.pageSize;
service
.filter(currentApp, currentEnv, null, pager.pageSize, from_, stateFilter)
.then(handleAPIResponse)
.then((result: any) => {
// Prepare pagination
let p = fixPagination(result.hits.total.value, pager, filterWorkers);
if (p) setPagination(p);
else return;
// Result
let workersList: { any }[] = [];
result.hits.hits.forEach(function (hit) {
workersList.push(hit._source);
});
setWorkers(workersList);
}, handleAPIError)
.catch(handleAPIError)
.finally(() => setLoading(false));
}
function getWorkerByHostname(hostname: string) {
if (!currentApp) return;
service
.getById(currentApp, hostname)
.then(handleAPIResponse)
.then((result: any) => {
if (result.hits.total == 0) {
message.warning(
"Worker not found maybe it's expired and got deleted from memory"
);
return;
}
handleShowWorkerDetails(result.hits.hits[0]._source);
}, handleAPIError)
.catch(handleAPIError);
}
/** ======================
* Hooks
---------------------- */
useEffect(() => {
refresh(pagination);
}, [stateFilter, currentApp]);
useEffect(() => {
if (qpHostname) {
getWorkerByHostname(qpHostname);
}
}, []);
/** ======================
* UI Callbacks
---------------------- */
function handleWorkerDetailsDrawerClosed() {
setDetailsVisible(false);
setQPHostname(undefined);
}
function handleRefreshWorkers() {
refresh();
}
function handleShowTotal(total) {
return `Total of ${total} workers`;
}
function handleTableChange(pagination, filters, sorter) {
refresh(pagination);
}
function handleStateFilterChange(e) {
setStateFilter(e.target.value);
}
function handleShowWorkerDetails(worker) {
setCurrentWorker(worker);
setQPHostname(worker.hostname);
setDetailsVisible(true);
}
function refresh(pager = { current: 1, pageSize: 10 }) {
setWorkers([]);
filterWorkers(pager);
}
return (
<>
<Helmet>
<html lang="en" />
<title>Workers</title>
<meta name="description" content="List of workers" />
<meta name="keywords" content="celery, workers" />
</Helmet>
<Row>
<Card
style={{ width: "100%" }}
bodyStyle={{ paddingBottom: 0, paddingRight: 0, paddingLeft: 0 }}
title={
<Row align="middle">
<Col span={21}>
<Radio.Group
onChange={handleStateFilterChange}
defaultValue="HEARTBEAT"
size="small"
style={{ fontWeight: 400 }}
>
<Radio.Button value="" style={{ fontStyle: "normal" }}>
ANY
</Radio.Button>
<Radio.Button value="HEARTBEAT">HEARTBEAT</Radio.Button>
<Radio.Button value="ONLINE">ONLINE</Radio.Button>
<Radio.Button value="OFFLINE">OFFLINE</Radio.Button>
</Radio.Group>
</Col>
<Col span={3}>
<Button
onClick={handleRefreshWorkers}
icon={<SyncOutlined />}
style={{ float: "right" }}
size="small"
/>
</Col>
</Row>
}
size="small"
>
<Table
dataSource={workers}
columns={columns}
loading={loading}
size="small"
rowKey="hostname"
style={{ width: "100%" }}
scroll={{ x: "100%" }}
showHeader={false}
pagination={{ ...pagination, showTotal: handleShowTotal }}
onChange={handleTableChange}
locale={{
emptyText: (
<div style={{ textAlign: "center" }}>
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={
<span>
No <a href="#API">workers</a> found
</span>
}
/>
</div>
),
}}
onRow={(record, rowIndex) => {
return {
onClick: (event) => {
handleShowWorkerDetails(record);
},
};
}}
/>
</Card>
</Row>
<Drawer
width="50vw"
placement="right"
closable={false}
onClose={handleWorkerDetailsDrawerClosed}
visible={workerDetailsVisible}
>
<WorkerDetailsDrawer worker={currentWorker} />
</Drawer>
</>
);
}
Example #28
Source File: tasks.tsx From leek with Apache License 2.0 | 4 votes |
TasksPage: React.FC = () => {
// STATE
const service = new TaskService();
const controlService = new ControlService();
const { currentApp, currentEnv } = useApplication();
// Filters
const [filters, setFilters] = useState<any>();
const [timeFilters, setTimeFilters] = useState<any>({
timestamp_type: "timestamp",
interval_type: "past",
offset: 900000,
});
const [order, setOrder] = useState<string>("desc");
// Data
const columns = TaskDataColumns();
const [pagination, setPagination] = useState<any>({
pageSize: 20,
current: 1,
});
const [loading, setLoading] = useState<boolean>();
const [tasksRetrying, setTasksRetrying] = useState<boolean>();
const [tasks, setTasks] = useState<any[]>([]);
// API Calls
function filterTasks(pager = { current: 1, pageSize: 20 }) {
if (!currentApp) return;
setLoading(true);
let allFilters = {
...filters,
...timeFilters,
};
let from_ = (pager.current - 1) * pager.pageSize;
service
.filter(currentApp, currentEnv, pager.pageSize, from_, order, allFilters)
.then(handleAPIResponse)
.then((result: any) => {
// Prepare pagination
let p = fixPagination(result.hits.total.value, pager, filterTasks);
if (p) setPagination(p);
else return;
// Result
let tasksList: { any }[] = [];
result.hits.hits.forEach(function (hit) {
tasksList.push(hit._source);
});
setTasks(tasksList);
}, handleAPIError)
.catch(handleAPIError)
.finally(() => setLoading(false));
}
// Hooks
useEffect(() => {
refresh(pagination);
}, [currentApp, currentEnv, filters, timeFilters, order]);
useEffect(() => {
//console.log(tasks)
}, [tasks]);
// UI Callbacks
function refresh(pager = { current: 1, pageSize: 20 }) {
filterTasks(pager);
}
function handleShowTaskDetails(record) {
window.open(
`/task?app=${currentApp}&env=${currentEnv}&uuid=${record.uuid}`,
"_blank"
);
}
function handleRefresh() {
refresh(pagination);
}
function handleShowTotal(total) {
return `Total of ${total} tasks`;
}
function handleTableChange(pagination, filters, sorter) {
refresh(pagination);
}
function handleFilterChange(values) {
setFilters(values);
}
function prepareList(items) {
return (
<List
header={
<Row justify="center">
<Text strong>Ineligible Tasks IDs</Text>
</Row>
}
dataSource={items}
style={{ maxHeight: 200, overflow: "auto" }}
size="small"
bordered
renderItem={(item) => <List.Item>{item}</List.Item>}
/>
);
}
function bulkRetryConfirmation(result) {
if (result.eligible_tasks_count == 0) {
message.warning("Found no eligible tasks for retrying!");
return;
}
confirm({
title: "Retry Filtered Tasks",
icon: <ExclamationCircleOutlined />,
content: (
<>
<Typography.Paragraph>
Do you really want to retry filtered tasks?
<ul>
<li>
{result.eligible_tasks_count} tasks are eligible to be retried.
</li>
<li>
{result.ineligible_tasks_count} tasks are not eligible to be
retried.
</li>
</ul>
</Typography.Paragraph>
{result.ineligible_tasks_count > 0 &&
prepareList(result.ineligible_tasks_ids)}
</>
),
onOk() {
return retryFiltered(false);
},
});
}
function pendingBulkRetry(result) {
confirm({
title: "Bulk tasks retry initiated!",
icon: <CheckCircleOutlined style={{ color: "#00BFA6" }} />,
content: (
<>
<Typography.Paragraph>
Tasks queued to the broker, you can filter the retried tasks using
the client name.
<ul>
<li>
Client name:{" "}
<Text copyable code>
{result.origin}
</Text>
</li>
<li>{result.succeeded_retries_count} tasks set to retry.</li>
<li>{result.failed_retries_count} tasks could not be retried.</li>
</ul>
</Typography.Paragraph>
{result.failed_retries_count > 0 &&
prepareList(result.failed_retries)}
</>
),
okText: "Ok",
cancelButtonProps: { style: { display: "none" } },
});
}
function retryFiltered(dryRun) {
if (!currentApp || !currentEnv) return;
setTasksRetrying(true);
let allFilters = { ...filters, ...timeFilters };
return controlService
.retryTasksByQuery(currentApp, currentEnv, allFilters, dryRun)
.then(handleAPIResponse)
.then((result: any) => {
if (dryRun) {
bulkRetryConfirmation(result);
} else {
pendingBulkRetry(result);
}
}, handleAPIError)
.catch(handleAPIError)
.finally(() => setTasksRetrying(false));
}
function handleRetryFiltered() {
retryFiltered(true);
}
return (
<>
<Helmet>
<html lang="en" />
<title>Tasks</title>
<meta name="description" content="List of tasks" />
<meta name="keywords" content="celery, tasks" />
</Helmet>
<Row style={{ marginBottom: "16px" }} gutter={[12, 12]}>
<Col xxl={5} xl={6} md={7} lg={8} sm={24} xs={24}>
<AttributesFilter
onFilter={handleFilterChange}
filters={timeFilters}
/>
</Col>
<Col xxl={19} xl={18} md={17} lg={16} sm={24} xs={24}>
<Row justify="center" style={{ width: "100%" }}>
<Card
bodyStyle={{ paddingBottom: 0, paddingRight: 0, paddingLeft: 0 }}
size="small"
style={{ width: "100%" }}
title={
<Row align="middle">
<Col span={3}>
<Switch
defaultChecked={order == "desc"}
style={{ marginLeft: "10px" }}
onChange={(e) => {
setOrder(e ? "desc" : "asc");
}}
size="small"
checkedChildren={
<CaretUpOutlined style={{ color: "#333" }} />
}
unCheckedChildren={<CaretDownOutlined />}
/>
</Col>
<Col span={18} style={{ textAlign: "center" }}>
<TimeFilter
timeFilter={timeFilters}
onTimeFilterChange={setTimeFilters}
/>
</Col>
<Col span={3}>
<Space style={{ float: "right" }}>
{filters &&
filters.state &&
filters.state.length &&
filters.state.every((s) =>
TerminalStates.includes(s)
) && (
<Button
ghost
type="primary"
size="small"
onClick={handleRetryFiltered}
loading={tasksRetrying}
>
Retry Filtered
</Button>
)}
<Button
size="small"
onClick={handleRefresh}
icon={<SyncOutlined />}
/>
</Space>
</Col>
</Row>
}
>
<Table
dataSource={tasks}
columns={columns}
pagination={{ ...pagination, showTotal: handleShowTotal }}
loading={loading}
size="small"
rowKey="uuid"
showHeader={false}
onChange={handleTableChange}
style={{ width: "100%" }}
scroll={{ x: "100%" }}
locale={{
emptyText: (
<div style={{ textAlign: "center" }}>
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={
<span>
No <a href="#API">tasks</a> found
</span>
}
/>
</div>
),
}}
onRow={(record, rowIndex) => {
return {
onClick: (event) => {
handleShowTaskDetails(record);
},
};
}}
/>
</Card>
</Row>
</Col>
</Row>
</>
);
}
Example #29
Source File: queues.tsx From leek with Apache License 2.0 | 4 votes |
QueuesPage = () => {
const columns = QueueDataColumns();
const service = new QueueService();
const [loading, setLoading] = useState<boolean>();
const [queues, setQueues] = useState<any>([]);
const { currentApp, currentEnv } = useApplication();
const [pagination, setPagination] = useState<any>({
pageSize: 10,
current: 1,
});
const [timeFilters, setTimeFilters] = useState<any>({
timestamp_type: "timestamp",
interval_type: "past",
offset: 900000,
});
function filterQueues(pager = { current: 1, pageSize: 10 }) {
if (!currentApp) return;
setLoading(true);
service
.filter(currentApp, currentEnv, timeFilters)
.then(handleAPIResponse)
.then((result: any) => {
setQueues(
result.aggregations.queues.buckets.map(
({ key, doc_count, state }) => {
let tasksStatesSeries = {
QUEUED: 0,
RECEIVED: 0,
STARTED: 0,
SUCCEEDED: 0,
FAILED: 0,
REJECTED: 0,
REVOKED: 0,
RETRY: 0,
RECOVERED: 0,
CRITICAL: 0,
};
const states = state.buckets.reduce((result, item) => {
result[item.key] = item.doc_count;
return result;
}, tasksStatesSeries);
return {
queue: key,
doc_count: doc_count,
...states,
};
}
)
);
}, handleAPIError)
.catch(handleAPIError)
.finally(() => setLoading(false));
}
useEffect(() => {
refresh(pagination);
}, [currentApp, currentEnv, timeFilters]);
// UI Callbacks
function refresh(pager = { current: 1, pageSize: 10 }) {
filterQueues(pager);
}
function handleShowTotal(total) {
return `Total of ${total} queues`;
}
function handleRefresh() {
refresh(pagination);
}
return (
<>
<Helmet>
<html lang="en" />
<title>Queues</title>
<meta name="description" content="Broker queues" />
<meta name="keywords" content="celery, queues" />
</Helmet>
<Row justify="center" style={{ width: "100%", marginTop: 13 }}>
<Alert
type="warning"
showIcon
closable
message="For monitoring queues, you should enable task_send_sent_event celery parameter on clients level!"
action={
<a
target="_blank"
rel="noopener norefferer"
href="https://tryleek.com/docs/introduction/requirements#enable-celery-task_send_sent_event"
>
<Button size="small" type="text">
Details
</Button>
</a>
}
/>
</Row>
<Row justify="center" style={{ width: "100%", marginTop: 13 }}>
<Card
bodyStyle={{ paddingBottom: 0, paddingRight: 0, paddingLeft: 0 }}
size="small"
style={{ width: "100%" }}
title={
<Row align="middle">
<Col span={3}></Col>
<Col span={18} style={{ textAlign: "center" }}>
<TimeFilter
timeFilter={timeFilters}
onTimeFilterChange={setTimeFilters}
/>
</Col>
<Col span={3}>
<Button
size="small"
onClick={handleRefresh}
icon={<SyncOutlined />}
style={{ float: "right" }}
/>
</Col>
</Row>
}
>
<Table
dataSource={queues}
columns={columns}
loading={loading}
pagination={{ ...pagination, showTotal: handleShowTotal }}
size="small"
rowKey="queue"
style={{ width: "100%" }}
scroll={{ x: "100%" }}
locale={{
emptyText: (
<div style={{ textAlign: "center" }}>
<Empty
image={Empty.PRESENTED_IMAGE_SIMPLE}
description={
<span>
No <a href="#API">queues</a> found
</span>
}
/>
</div>
),
}}
/>
</Card>
</Row>
</>
);
}