@ant-design/icons#LinkOutlined TypeScript Examples
The following examples show how to use
@ant-design/icons#LinkOutlined.
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: ActionAttribute.tsx From posthog-foss with MIT License | 6 votes |
export function ActionAttribute({ attribute, value }: { attribute: string; value?: string }): JSX.Element {
const icon =
attribute === 'text' ? (
<FontSizeOutlined />
) : attribute === 'href' ? (
<LinkOutlined />
) : attribute === 'selector' ? (
<BranchesOutlined />
) : (
<FormOutlined />
)
const text =
attribute === 'href' ? (
<a href={value} target="_blank" rel="noopener noreferrer">
{value}
</a>
) : attribute === 'selector' ? (
value ? (
<span style={{ fontFamily: 'monospace' }}>
<SelectorString value={value} />
</span>
) : (
<span>
Could not generate a unique selector for this element. Please instrument it with a unique{' '}
<code>id</code> or <code>data-attr</code> attribute.
</span>
)
) : (
value
)
return (
<div key={attribute} style={{ marginBottom: 10, paddingLeft: 24, position: 'relative' }}>
<div style={{ position: 'absolute', left: 2, top: 3, color: 'hsl(240, 14%, 50%)' }}>{icon}</div>
<span>{text}</span>
</div>
)
}
Example #2
Source File: index.tsx From XFlow with MIT License | 6 votes |
GraphToolbar = (props: Props) => {
const { onAddNodeClick, onDeleteNodeClick, onConnectEdgeClick } = props
const [selectedNodes, setSelectedNodes] = React.useState([])
/** 监听画布中选中的节点 */
const watchModelService = async () => {
const appRef = useXFlowApp()
const modelService = appRef && appRef?.modelService
if (modelService) {
const model = await MODELS.SELECTED_NODES.getModel(modelService)
model.watch(async () => {
const nodes = await MODELS.SELECTED_NODES.useValue(modelService)
setSelectedNodes(nodes)
})
}
}
watchModelService()
return (
<div className="xflow-er-solution-toolbar">
<div className="icon" onClick={() => onAddNodeClick()}>
<span>添加节点</span>
<PlusCircleOutlined />
</div>
<div className="icon" onClick={() => onConnectEdgeClick()}>
<span>添加关系</span>
<LinkOutlined />
</div>
<div
className={`icon ${selectedNodes?.length > 0 ? '' : 'disabled'}`}
onClick={() => onDeleteNodeClick()}
>
<span>删除节点</span>
<DeleteOutlined />
</div>
</div>
)
}
Example #3
Source File: StatusIcon.tsx From datart with Apache License 2.0 | 6 votes |
CanLinkageIcon: React.FC<{
title: React.ReactNode | undefined;
}> = ({ title }) => {
return (
<Tooltip title={title}>
<Button icon={<LinkOutlined style={{ color: PRIMARY }} />} type="link" />
</Tooltip>
);
}
Example #4
Source File: CustomInlineEditor.tsx From dnde with GNU General Public License v3.0 | 5 votes |
LinkItem = ({ setLinkCallback }: LinkItemProps) => {
const [active, setActive] = useState(false);
const [link, setLink] = useState('');
const linkRef = useRef<any>(null);
const onChange = (e: any) => {
setLink(e.target.value);
};
const onMouseDown = (e: any) => {
restoreSelection();
};
return (
<Popover
visible={active}
content={
<div style={{ display: 'flex', gap: '8px' }}>
<Input
className="inline-editor-link"
ref={linkRef}
value={link}
onMouseDown={onMouseDown}
onChange={onChange}
placeholder="link"
/>
<Button
type="default"
onMouseDown={ResetEventBehaviour}
onClick={() => {
setActive(false);
restoreSelection();
document.execCommand(
'insertHTML',
false,
'<a href="' + addHttps(link) + '" target="_blank">' + document.getSelection() + '</a>'
);
}}
>
create
</Button>
</div>
}
trigger="click"
placement="bottom"
>
<Button
style={{ fontSize: '12px' }}
onClick={(e) => {
ResetEventBehaviour(e);
setActive(!active);
setLink('');
}}
size="small"
icon={<LinkOutlined />}
></Button>
</Popover>
);
}
Example #5
Source File: ProfileCard.tsx From office-hours with GNU General Public License v3.0 | 5 votes |
export default function ProfileCard({
name,
role,
linkedin,
personalSite,
imgSrc,
}: {
name: string;
role: string;
linkedin?: string;
personalSite?: string;
imgSrc: string;
}): ReactElement {
return (
<StyledCard>
<img width={200} alt={`${name}'s profile image`} src={imgSrc} />
<ImageOverlay />
<CardContents>
<CardTitle>{name}</CardTitle>
<div>{role}</div>
<LinkIcons>
{linkedin && (
<NavyLink href={linkedin} target="_blank" rel="noopener noreferrer">
<LinkedinFilled title="LinkedIn" style={{ cursor: "pointer" }} />
</NavyLink>
)}
{personalSite && (
<NavyLink
href={personalSite}
target="_blank"
rel="noopener noreferrer"
>
<LinkOutlined
title="Personal Website"
style={{ cursor: "pointer" }}
/>
</NavyLink>
)}
</LinkIcons>
</CardContents>
</StyledCard>
);
}
Example #6
Source File: ContextItem.spec.tsx From next-basics with GNU General Public License v3.0 | 5 votes |
describe("ContextItem", () => {
it("should work", () => {
const handleItemClick = jest.fn();
const handleItemDelete = jest.fn();
const handleDropItem = jest.fn();
const handleItemHover = jest.fn();
const wrapper = shallow(
<ContextItem
data={{
name: "data-b",
value: {
id: 1,
},
}}
handleItemClick={handleItemClick}
handleItemDelete={handleItemDelete}
handleDropItem={handleDropItem}
index={1}
canDrag={true}
handleItemHover={handleItemHover}
/>
);
expect(wrapper.find(CodeOutlined).length).toBe(1);
wrapper.find(".deleteIcon").simulate("click");
expect(handleItemDelete).toBeCalled();
wrapper.setProps({
data: {
name: "data-a",
resolve: {
useProvider: "provider-a",
args: ["args1"],
if: false,
transform: {
value: "<% DATA %>",
},
},
},
});
expect(wrapper.find(LinkOutlined).length).toBe(1);
wrapper.find(".varItem").simulate("click");
expect(handleItemClick).toBeCalled();
wrapper.find(".varItem").invoke("onMouseEnter")({} as any);
expect(handleItemHover).toBeCalledWith("data-a");
handleItemHover.mockClear();
wrapper.find(".varItem").invoke("onMouseLeave")({} as any);
expect(handleItemHover).toBeCalledWith();
});
});
Example #7
Source File: ContextItem.tsx From next-basics with GNU General Public License v3.0 | 5 votes |
export function ContextItem({
index,
data,
canDrag,
highlighted,
handleDropItem,
handleItemClick,
handleItemDelete,
handleItemHover,
}: ContextItemProps): React.ReactElement {
const ref = useRef();
const [{ isOver, dropClassName }, drop] = useDrop({
accept: type,
collect: (monitor) => {
const { index: dragIndex } = monitor.getItem() || {};
if (dragIndex === index) {
return {};
}
return {
isOver: monitor.isOver(),
dropClassName:
dragIndex < index
? `${styles.dropOverDownward}`
: `${styles.dropOverUpward}`,
};
},
drop: (item: any) => {
handleDropItem(item.index, index);
},
});
const [{ isDragging }, drag] = useDrag({
item: { type, index },
canDrag,
collect: (monitor) => ({
isDragging: monitor.isDragging(),
}),
});
drop(drag(ref));
const handleMouseEnter = (): void => {
handleItemHover(data.name);
};
const handleMouseLeave = (): void => {
handleItemHover();
};
return (
<div
ref={ref}
className={classNames(styles.varItem, {
[dropClassName]: isOver,
[styles.highlighted]: highlighted,
})}
onClick={handleItemClick}
key={data.name}
onMouseEnter={handleMouseEnter}
onMouseLeave={handleMouseLeave}
>
{data.resolve ? (
<LinkOutlined style={{ color: "var(--theme-orange-color)" }} />
) : (
<CodeOutlined style={{ color: "var(--theme-green-color)" }} />
)}
<span className={styles.varName}>{data.name}</span>
<Button
type="link"
danger
icon={<DeleteOutlined />}
className={styles.deleteIcon}
onClick={handleItemDelete}
/>
</div>
);
}
Example #8
Source File: EntityPicker.tsx From jmix-frontend with Apache License 2.0 | 5 votes |
export function EntityPicker(props: EntityPickerProps) {
const {
value,
entityName,
onChange,
disabled,
propertyName,
associationOptions
} = props;
const screens = useScreens();
const intl = useIntl();
const metadata = useMetadata();
const propertyInfo = useMemo(() => {
return getPropertyInfo(metadata.entities, entityName, propertyName);
}, [metadata.entities, entityName, propertyName]);
if(propertyInfo == null) {
throw new Error(`Metadata not found for property ${propertyName} of entity ${entityName}`);
}
const isCorrectFiekd = useMemo(() => {
return isToOneAssociation(propertyInfo)
}, [propertyInfo]);
if(!isCorrectFiekd) {
throw new Error(`property must be a to-one association`);
}
const displayedValue = useMemo(() => {
return value != null
? getDisplayedValue(value, associationOptions)
: undefined
}, [value, associationOptions])
const onSelectEntity = useCallback((entityInstance?: Record<string, unknown>) => {
if (onChange != null) {
const newValue = entityInstance?.id != null
? entityInstance.id as string
: undefined
onChange(newValue);
}
}, [onChange, propertyInfo]);
const handleClick = useCallback(() => {
try{
openCrudScreen({
entityName: propertyInfo.type,
crudScreenType: "entityList",
screens,
props: {
onSelectEntity
}
})
} catch(_e) {
notifications.show({
type: NotificationType.ERROR,
description: intl.formatMessage({ id: "common.openScreenError" }, {entityName: propertyInfo.type})
});
}
}, [entityName, screens, onSelectEntity, propertyInfo.type]);
return (
<Input
prefix={<LinkOutlined />}
readOnly={true}
onClick={handleClick}
value={displayedValue}
disabled={disabled}
id={propertyName}
/>
);
}
Example #9
Source File: Spacing.tsx From yugong with MIT License | 4 votes |
Spacing: React.FC<Props> = ({ unit, onChange, margin, padding }) => {
const [spaceType, setSpaceType] = useState<"margin" | "padding">("padding");
const [inValues, setInValues] = useState<any[]>(defaultVal);
const [outValues, setOutValues] = useState<any[]>(defaultVal);
const [locked, setLocked] = useState<boolean>();
useEffect(() => {
setInValues(padding || defaultVal);
}, [padding]);
useEffect(() => {
setOutValues(margin || defaultVal);
}, [margin]);
const onChangeValue = useCallback(
(index: number) => (value: UnitType) => {
if (spaceType === "padding") {
const values: any[] = [...inValues];
values[index] = value;
if (locked === true) {
values[1] = values[2] = values[3] = values[0] = value;
}
setInValues(values);
onChange(spaceType, values);
}
if (spaceType === "margin") {
const values = [...outValues];
values[index] = value;
if (locked === true) {
values[1] = values[2] = values[3] = values[0] = value;
}
setOutValues(values);
onChange(spaceType, values);
}
},
[spaceType, inValues, locked, onChange, outValues]
);
const getValue = useCallback(
(type = spaceType) => {
let values: any[] = [];
if (type === "padding") {
values = inValues;
}
if (type === "margin") {
values = outValues;
}
return values;
},
[inValues, outValues, spaceType]
);
const onChangeType = useCallback(
(type: SpaceType) => (e: any) => {
e.stopPropagation();
setSpaceType(type);
const values = getValue(type);
const unEqu = values.filter((item) => values[0] !== item);
if (!!unEqu.length) {
setLocked(false);
} else {
setLocked(true);
}
},
[getValue, setLocked]
);
const onToggleLocker = useCallback(() => {
setLocked(!locked);
}, [locked]);
const setLabel = useCallback((index: number) => {
switch (index) {
case 0:
return "顶部";
case 1:
return "右边";
case 2:
return "底部";
case 3:
return "左边";
default:
return "";
}
}, []);
return (
<>
<Divider orientation="left"><span className={s.divide}>边距</span></Divider>
<Row gutter={4}>
<Col span={9}>
<div
className={s.boxA}
onClick={onChangeType("margin")}
style={
spaceType === "margin"
? { backgroundColor: "#fff" }
: { backgroundColor: "#eee" }
}
>
<div
className={s.boxB}
onClick={onChangeType("padding")}
style={
spaceType === "padding"
? { backgroundColor: "#fff" }
: { backgroundColor: "#eee" }
}
/>
</div>
</Col>
<Col span={3} className={s.middle}>
<LinkOutlined
onClick={onToggleLocker}
className={locked ? s.locked : undefined}
/>
</Col>
<Col span={12}>
{getValue().map((item, index) => {
return <Unitinput
span={{ label: 3, wrapper: 21 }}
key={`${spaceType}${index}`}
className={s.unititem}
label={setLabel(index)}
defaultValue={item}
onChange={onChangeValue(index)}
/>;
})}
</Col>
</Row>
</>
);
}
Example #10
Source File: FileManager.tsx From anew-server with MIT License | 4 votes |
FileManager: React.FC<FileManagerProps> = (props) => {
const { modalVisible, handleChange, connectId } = props;
const [columnData, setColumnData] = useState<API.SSHFileList[]>([]);
const [showHidden, setShowHidden] = useState<boolean>(false);
const [childrenDrawer, setChildrenDrawer] = useState<boolean>(false);
const [currentPathArr, setCurrentPathArr] = useState<string[]>([]);
const [initPath, setInitPath] = useState<string>('');
const _dirSort = (item: API.SSHFileList) => {
return item.isDir;
};
const getFileData = (key: string, path: string) => {
querySSHFile(key, path).then((res) => {
const obj = lds.orderBy(res.data, [_dirSort, 'name'], ['desc', 'asc']);
showHidden ? setColumnData(obj) : setColumnData(obj.filter((x) => !x.name.startsWith('.')));
try {
// 获取服务器的当前路径
let pathb = obj[0].path;
const index = pathb.lastIndexOf('/');
pathb = pathb.substring(0, index + 1);
setCurrentPathArr(pathb.split('/').filter((x: any) => x !== ''));
setInitPath(pathb); // 保存当前路径,刷新用
} catch (exception) {
setCurrentPathArr(path.split('/').filter((x) => x !== ''));
setInitPath(path);
}
});
};
const getChdirDirData = (key: string, path: string) => {
const index = currentPathArr.indexOf(path);
const currentDir = '/' + currentPathArr.splice(0, index + 1).join('/');
getFileData(key, currentDir);
};
const handleDelete = (key: string, path: string) => {
if (!path) return;
const index = path.lastIndexOf('/');
const currentDir = path.substring(0, index + 1);
const currentFile = path.substring(index + 1, path.length);
const content = `您是否要删除 ${currentFile}?`;
Modal.confirm({
title: '注意',
content,
onOk: () => {
deleteSSHFile(key, path).then((res) => {
if (res.code === 200 && res.status === true) {
message.success(res.message);
getFileData(key, currentDir);
}
});
},
onCancel() { },
});
};
const handleDownload = (key: string, path: string) => {
if (!path) return;
const index = path.lastIndexOf('/');
const currentFile = path.substring(index + 1, path.length);
const content = `您是否要下载 ${currentFile}?`;
Modal.confirm({
title: '注意',
content,
onOk: () => {
const token = localStorage.getItem('token');
const link = document.createElement('a');
link.href = `/api/v1/host/ssh/download?key=${key}&path=${path}&token=${token}`;
document.body.appendChild(link);
const evt = document.createEvent('MouseEvents');
evt.initEvent('click', false, false);
link.dispatchEvent(evt);
document.body.removeChild(link);
},
onCancel() { },
});
};
const uploadProps = {
name: 'file',
action: `/api/v1/host/ssh/upload?key=${connectId}&path=${initPath}`,
multiple: true,
headers: {
Authorization: `Bearer ${localStorage.getItem('token')}`,
},
// showUploadList: {
// removeIcon: false,
// showRemoveIcon: false,
// },
onChange(info: any) {
// if (info.file.status !== 'uploading') {
// console.log(info.file, info.fileList);
// }
//console.log(info);
if (info.file.status === 'done') {
message.success(`${info.file.name} file uploaded successfully`);
getFileData(connectId, initPath as string); // 刷新数据
} else if (info.file.status === 'error') {
message.error(`${info.file.name} file upload failed.`);
}
},
progress: {
strokeColor: {
'0%': '#108ee9',
'100%': '#87d068',
},
strokeWidth: 3,
format: (percent: any) => `${parseFloat(percent.toFixed(2))}%`,
},
};
const columns: ProColumns<API.SSHFileList>[] = [
{
title: '名称',
dataIndex: 'name',
render: (_, record) =>
record.isDir ? (
<div onClick={() => getFileData(connectId, record.path)} style={{ cursor: 'pointer' }}>
<FolderTwoTone />
<span style={{ color: '#1890ff', paddingLeft: 5 }}>{record.name}</span>
</div>
) : (
<React.Fragment>
{record.isLink ? (
<div>
<LinkOutlined />
<Tooltip title="Is Link">
<span style={{ color: '#3cb371', paddingLeft: 5 }}>{record.name}</span>
</Tooltip>
</div>
) : (
<div>
<FileOutlined />
<span style={{ paddingLeft: 5 }}>{record.name}</span>
</div>
)}
</React.Fragment>
),
},
{
title: '大小',
dataIndex: 'size',
},
{
title: '修改时间',
dataIndex: 'mtime',
},
{
title: '属性',
dataIndex: 'mode',
},
{
title: '操作',
dataIndex: 'option',
valueType: 'option',
render: (_, record) =>
!record.isDir && !record.isLink ? (
<>
<Tooltip title="下载文件">
<DownloadOutlined
style={{ fontSize: '17px', color: 'blue' }}
onClick={() => handleDownload(connectId, record.path)}
/>
</Tooltip>
<Divider type="vertical" />
<Tooltip title="删除文件">
<DeleteOutlined
style={{ fontSize: '17px', color: 'red' }}
onClick={() => handleDelete(connectId, record.path)}
/>
</Tooltip>
</>
) : null,
},
];
useEffect(() => {
// 是否显示隐藏文件
getFileData(connectId, initPath as string); // 刷新数据
}, [showHidden]);
const { Dragger } = Upload;
return (
<Drawer
title="文件管理器"
placement="right"
width={800}
visible={modalVisible}
onClose={()=>handleChange(false)}
getContainer={false}
>
{/* <input style={{ display: 'none' }} type="file" ref={(ref) => (this.input = ref)} /> */}
<div className={styles.drawerHeader}>
<Breadcrumb>
<Breadcrumb.Item href="#" onClick={() => getFileData(connectId, '/')}>
<ApartmentOutlined />
</Breadcrumb.Item>
<Breadcrumb.Item href="#" onClick={() => getFileData(connectId, '')}>
<HomeOutlined />
</Breadcrumb.Item>
{currentPathArr.map((item) => (
<Breadcrumb.Item key={item} href="#" onClick={() => getChdirDirData(connectId, item)}>
<span>{item}</span>
</Breadcrumb.Item>
))}
</Breadcrumb>
<div style={{ display: 'flex', alignItems: 'center' }}>
<span>显示隐藏文件:</span>
<Switch
checked={showHidden}
checkedChildren="开启"
unCheckedChildren="关闭"
onChange={(v) => {
setShowHidden(v);
}}
/>
<Button
style={{ marginLeft: 10 }}
size="small"
type="primary"
icon={<UploadOutlined />}
onClick={() => setChildrenDrawer(true)}
>
上传文件
</Button>
</div>
</div>
<Drawer
title="上传文件"
width={320}
closable={false}
onClose={() => setChildrenDrawer(false)}
visible={childrenDrawer}
>
<div style={{ height: 150 }}>
<Dragger {...uploadProps}>
<p className="ant-upload-drag-icon">
<InboxOutlined />
</p>
<p className="ant-upload-text">单击或拖入上传</p>
<p className="ant-upload-hint">支持多文件</p>
</Dragger>
</div>
</Drawer>
<ProTable
pagination={false}
search={false}
toolBarRender={false}
rowKey="name"
dataSource={columnData}
columns={columns}
/>
</Drawer>
);
}
Example #11
Source File: LinkageFields.tsx From datart with Apache License 2.0 | 4 votes |
LinkageFields: React.FC<LinkageFieldsProps> = memo(
({ form, viewMap, curWidget, chartGroupColumns }) => {
const t = useI18NPrefix(`viz.linkage`);
const renderOptions = useCallback(
(index: number, key: 'triggerViewId' | 'linkerViewId') => {
const viewLinkages: ViewLinkageItem[] =
form?.getFieldValue('viewLinkages');
if (!viewLinkages) {
return null;
}
if (key === 'triggerViewId') {
return chartGroupColumns?.map(item => (
<Option
key={item.uid}
fieldvaluetype={item.type}
value={item.colName}
>
<div style={{ display: 'flex', justifyContent: 'space-between' }}>
<span>{item.colName}</span>
<FieldType>{item.type}</FieldType>
</div>
</Option>
));
} else if (key === 'linkerViewId') {
return viewMap[viewLinkages[index][key]].meta
?.filter(item => {
const enableTypes = [
DataViewFieldType.STRING,
DataViewFieldType.DATE,
];
return item.type && enableTypes.includes(item.type);
})
.map(item => (
<Option key={item.id} fieldvaluetype={item.type} value={item.id}>
<div
style={{ display: 'flex', justifyContent: 'space-between' }}
>
<span>{item.id}</span>
<FieldType>{item.type}</FieldType>
</div>
</Option>
));
}
},
[chartGroupColumns, form, viewMap],
);
const getItem = useCallback(
(index: number) => {
const viewLinkages: ViewLinkageItem[] =
form?.getFieldValue('viewLinkages');
return viewLinkages[index];
},
[form],
);
const getLinkerView = useCallback(
(index: number) => {
const viewLinkages: ViewLinkageItem[] =
form?.getFieldValue('viewLinkages');
if (!viewLinkages) {
return null;
}
return viewMap[viewLinkages[index].linkerViewId];
},
[form, viewMap],
);
return (
<Wrapper>
<Divider orientation="left">{t('associatedFields')}</Divider>
<div>
{t('dataSource')} : {viewMap[curWidget?.viewIds?.[0]]?.name}
</div>
<Form.List name="viewLinkages">
{(fields, _, { errors }) => {
return (
<FormWrap>
{fields.map((field, index) => (
<Form.Item noStyle key={index} shouldUpdate>
<div className="form-item">
<div className="form-item-start">
<Form.Item
{...field}
style={{ display: 'inline-block' }}
shouldUpdate
validateTrigger={['onChange', 'onClick', 'onBlur']}
name={[field.name, 'triggerColumn']}
fieldKey={[field.fieldKey, 'id']}
rules={[
{ required: true, message: t('selectTriggers') },
]}
>
<Select
style={{ width: 200 }}
showSearch
placeholder={t('selectTriggers')}
allowClear
>
{renderOptions(index, 'triggerViewId')}
</Select>
</Form.Item>
</div>
<div className="form-item-and">
<LinkOutlined />
</div>
<div className="form-item-endValue">
<Form.Item
{...field}
style={{ display: 'inline-block' }}
shouldUpdate
validateTrigger={['onChange', 'onClick', 'onBlur']}
name={[field.name, 'linkerColumn']}
rules={[
{ required: true, message: t('selectLinker') },
]}
fieldKey={[field.fieldKey, 'id']}
>
<Select
style={{ width: 200 }}
showSearch
placeholder={t('selectLinker')}
allowClear
>
{renderOptions(index, 'linkerViewId')}
</Select>
</Form.Item>
<span className="ViewName">
{' '}
( {getLinkerView(index)?.name} {' / '}
{getItem(index)?.linkerName})
</span>
</div>
</div>
</Form.Item>
))}
<Form.Item>
<Form.ErrorList errors={errors} />
</Form.Item>
{!fields.length && <Empty key="empty" />}
</FormWrap>
);
}}
</Form.List>
</Wrapper>
);
},
)
Example #12
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 #13
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 #14
Source File: chartGroup.tsx From fe-v5 with Apache License 2.0 | 4 votes |
// 根据chartConfigModal配置的数据进行展示
export default function ChartGroup(props: Props) {
const { t } = useTranslation();
const {
id,
cluster,
busiId,
groupInfo,
range,
step,
variableConfig,
onAddChart,
onUpdateChart,
onCloneChart,
onShareChart,
onDelChartGroup,
onDelChart,
onUpdateChartGroup,
onMoveUpChartGroup,
onMoveDownChartGroup,
moveUpEnable,
moveDownEnable,
} = props;
const [chartConfigs, setChartConfigs] = useState<Chart[]>([]);
const [Refs, setRefs] = useState<RefObject<any>[]>();
const [layout, setLayout] = useState<Layouts>(layouts); // const [colItem, setColItem] = useState<number>(defColItem);
const [mounted, setMounted] = useState<boolean>(false);
useEffect(() => {
init();
Refs &&
Refs.forEach((ref) => {
const graphInstance = ref.current;
graphInstance && graphInstance.refresh();
});
}, [groupInfo.updateTime, cluster]);
const init = async () => {
setMounted(false);
getCharts(busiId, groupInfo.id).then(async (res) => {
let charts = res.dat
? res.dat.map((item: { configs: string; id: any; weight: any; group_id: number }) => {
let configs = item.configs ? JSON.parse(item.configs) : {};
return { id: item.id, configs, weight: item.weight, group_id: item.group_id };
})
: [];
let haveNewChart = false;
const innerLayout = charts.map((item: { configs: { layout: { i: string; x?: number; y?: number; w: number; h: number } } }, index: string | number) => {
if (item.configs.layout) {
// 当Chart被删除后 layout中的i会中断,ResponsiveReactGridLayout会有问题
item.configs.layout.i = '' + index;
// 克隆图表后 layout 不具备 x/y 值,需要计算设置
if (item.configs.layout.x === undefined && item.configs.layout.y === undefined) {
haveNewChart = true;
return getNewItemLayout(
charts.slice(0, index).map(
(item: {
configs: {
layout: any;
};
}) => item.configs.layout,
),
Number(index),
{
w: item.configs.layout.w,
h: item.configs.layout.h,
},
);
}
return item.configs.layout;
} else {
haveNewChart = true;
return getNewItemLayout(
charts.slice(0, index).map(
(item: {
configs: {
layout: any;
};
}) => item.configs.layout,
),
Number(index),
);
}
});
if (haveNewChart) {
const { dat } = await getPerm(busiId, 'rw');
dat &&
updateCharts(
busiId,
charts.map((item, index) => {
const { h, w, x, y, i } = innerLayout[index];
item.configs.layout = { h, w, x, y, i };
return item;
}),
);
}
const realLayout: Layouts = { lg: innerLayout, sm: innerLayout, md: innerLayout, xs: innerLayout, xxs: innerLayout };
setLayout(realLayout);
setChartConfigs(charts);
setRefs(new Array(charts.length).fill(0).map((_) => React.createRef()));
setMounted(true);
});
};
const getNewItemLayout = function (curentLayouts: Array<Layout>, index: number, size?: { w: number; h: number }): Layout {
const { w, h } = size || { w: unit, h: unit / 3 };
const layoutArrayLayoutFillArray = new Array<Array<number>>();
curentLayouts.forEach((layoutItem) => {
if (layoutItem) {
const { w, h, x, y } = layoutItem;
for (let i = 0; i < h; i++) {
if (typeof layoutArrayLayoutFillArray[i + y] === 'undefined') {
layoutArrayLayoutFillArray[i + y] = new Array<number>(cols).fill(0);
}
for (let k = 0; k < w; k++) {
layoutArrayLayoutFillArray[i + y][k + x] = 1;
}
}
}
});
let nextLayoutX = -1;
let nextLayoutY = -1; // 填充空行
for (let i = 0; i < layoutArrayLayoutFillArray.length; i++) {
if (typeof layoutArrayLayoutFillArray[i] === 'undefined') {
layoutArrayLayoutFillArray[i] = new Array<number>(cols).fill(0);
}
}
function isEmpty(i: number, j: number) {
let flag = true;
for (let x = i; x < i + w; x++) {
for (let y = j; y < j + h; y++) {
if (layoutArrayLayoutFillArray[x] && layoutArrayLayoutFillArray[x][y]) {
flag = false;
}
}
}
return flag;
}
for (let i = 0; i < layoutArrayLayoutFillArray.length - 1; i++) {
for (let j = 0; j <= cols - unit; j++) {
if (isEmpty(i, j)) {
nextLayoutY = i;
nextLayoutX = j;
break;
}
}
}
if (nextLayoutX === -1) {
nextLayoutX = 0;
nextLayoutY = layoutArrayLayoutFillArray.length;
}
return { w, h, x: nextLayoutX, y: nextLayoutY, i: '' + index };
};
const onLayoutChange = async (val: { h: any; w: any; x: any; y: any; i: any }[]) => {
if (val.length === 0) return;
let needUpdate = false;
const { lg: lgLayout } = layout;
for (var k = 0; k < val.length; k++) {
const { h, w, x, y, i } = val[k];
const { h: oldh, w: oldw, x: oldx, y: oldy, i: oldi } = lgLayout[k];
if (h !== oldh || w !== oldw || x !== oldx || y !== oldy || i !== oldi) {
needUpdate = true;
}
}
if (!needUpdate) return;
let currConfigs = chartConfigs.map((item, index) => {
const { h, w, x, y, i } = val[index];
item.configs.layout = { h, w, x, y, i };
return item;
});
// setLayout({ lg: [...layout], sm: [...layout], md: [...layout], xs: [...layout], xxs: [...layout] });
const { dat } = await getPerm(busiId, 'rw');
dat && updateCharts(busiId, currConfigs);
};
const setArrange = async (colItem: number, w = cols / colItem, h = unit / 3) => {
setMounted(false);
let countX = 0;
let countY = 0;
const _lg: Layout[] = [];
[...layout.lg].forEach((ele, index) => {
let innerObj = { ...ele };
if (index + 1 > colItem) {
let c = (index + 1) / colItem;
countY = Math.trunc(c) * h;
}
innerObj.w = w;
innerObj.h = h;
innerObj.x = countX;
innerObj.y = countY;
countX += innerObj.w;
if ((index + 1) % colItem === 0) {
countX = 0;
}
_lg.push(innerObj);
});
let currConfigs = chartConfigs.map((item, index) => {
const { h, w, x, y, i } = _lg[index];
item.configs.layout = { h, w, x, y, i };
return item;
});
const { dat } = await getPerm(busiId, 'rw');
dat && updateCharts(busiId, currConfigs);
setLayout({ lg: [..._lg], sm: [..._lg], md: [..._lg], xs: [..._lg], xxs: [..._lg] });
setChartConfigs(currConfigs);
setMounted(true);
};
function handleMenuClick(e) {
e.domEvent.stopPropagation();
setArrange(Number(e.key));
}
function menu() {
const { t } = useTranslation();
let listArr: ReactElement[] = [];
for (let i = 1; i <= defColItem; i++) {
let item = (
<Menu.Item key={i}>
{i}
{t('列')}
</Menu.Item>
);
listArr.push(item);
}
return <Menu onClick={handleMenuClick}>{listArr}</Menu>;
}
const generateRightButton = () => {
const { t } = useTranslation();
return (
<>
<Button
type='link'
size='small'
onClick={(event) => {
event.stopPropagation();
onAddChart(groupInfo.id);
}}
>
{t('新增图表')}
</Button>
<Divider type='vertical' />
<Dropdown overlay={menu()}>
<Button
type='link'
size='small'
onClick={(event) => {
event.stopPropagation();
}}
>
{t('一键规整')}
<DownOutlined />
</Button>
</Dropdown>
<Divider type='vertical' />
<Button
type='link'
size='small'
onClick={(event) => {
event.stopPropagation();
onUpdateChartGroup(groupInfo);
}}
>
{t('修改')}
</Button>
<Divider type='vertical' />
<Button
type='link'
size='small'
disabled={!moveUpEnable}
onClick={(event) => {
event.stopPropagation();
onMoveUpChartGroup(groupInfo);
}}
>
{t('上移')}
</Button>
<Divider type='vertical' />
<Button
type='link'
size='small'
disabled={!moveDownEnable}
onClick={(event) => {
event.stopPropagation();
onMoveDownChartGroup(groupInfo);
}}
>
{t('下移')}
</Button>
<Divider type='vertical' />
<Button
type='link'
size='small'
onClick={(event) => {
event.stopPropagation();
confirm({
title: `${t('是否删除分类')}:${groupInfo.name}`,
onOk: async () => {
onDelChartGroup(groupInfo.id);
},
onCancel() {},
});
}}
>
{t('删除')}
</Button>
</>
);
};
const generateDOM = () => {
return (
chartConfigs &&
chartConfigs.length > 0 &&
chartConfigs.map((item, i) => {
let { QL, name, legend, yplotline1, yplotline2, highLevelConfig, version } = item.configs;
if (semver.valid(version)) {
// 新版图表配置的版本使用语义化版本规范
const { type } = item.configs as any;
return (
<div
style={{
border: '1px solid #e0dee2',
}}
key={String(i)}
>
<Renderer
dashboardId={id}
id={item.id}
time={range}
refreshFlag={props.refreshFlag}
step={step}
type={type}
values={item.configs as any}
variableConfig={variableConfig}
onCloneClick={() => {
onCloneChart(groupInfo, item);
}}
onShareClick={() => {
onShareChart(groupInfo, item);
}}
onEditClick={() => {
onUpdateChart(groupInfo, item);
}}
onDeleteClick={() => {
confirm({
title: `${t('是否删除图表')}:${item.configs.name}`,
onOk: async () => {
onDelChart(groupInfo, item);
},
});
}}
/>
</div>
);
}
const promqls = QL.map((item) =>
variableConfig && variableConfig.var && variableConfig.var.length ? replaceExpressionVars(item.PromQL, variableConfig, variableConfig.var.length, id) : item.PromQL,
);
const legendTitleFormats = QL.map((item) => item.Legend);
return (
<div
style={{
border: '1px solid #e0dee2',
}}
key={String(i)}
>
<Graph
ref={Refs![i]}
highLevelConfig={highLevelConfig}
data={{
yAxis: {
plotLines: [
{
value: yplotline1 ? yplotline1 : undefined,
color: 'orange',
},
{
value: yplotline2 ? yplotline2 : undefined,
color: 'red',
},
],
},
legend: legend,
step,
range,
title: (
<Tooltip title={name}>
<span>{name}</span>
</Tooltip>
),
promqls,
legendTitleFormats,
}}
extraRender={(graph) => {
return (
<>
<Button
type='link'
size='small'
onClick={(e) => {
e.preventDefault();
window.open(item.configs.link);
}}
disabled={!item.configs.link}
>
<LinkOutlined />
</Button>
<Button
type='link'
size='small'
onClick={(e) => {
e.preventDefault();
onUpdateChart(groupInfo, item);
}}
>
<EditOutlined />
</Button>
<Button
type='link'
size='small'
onClick={(e) => {
e.preventDefault();
confirm({
title: `${t('是否删除图表')}:${item.configs.name}`,
onOk: async () => {
onDelChart(groupInfo, item);
},
onCancel() {},
});
}}
>
<CloseCircleOutlined />
</Button>
</>
);
}}
/>
</div>
);
})
);
};
const renderCharts = useCallback(() => {
const { t } = useTranslation();
return (
<div
style={{
width: '100%',
}}
>
{chartConfigs && chartConfigs.length > 0 ? (
<ResponsiveReactGridLayout
cols={{ lg: cols, sm: cols, md: cols, xs: cols, xxs: cols }}
layouts={layout}
onLayoutChange={onLayoutChange}
measureBeforeMount={false}
useCSSTransforms={false}
preventCollision={false}
isBounded={true}
draggableHandle='.graph-header'
>
{generateDOM()}
</ResponsiveReactGridLayout>
) : (
<p className='empty-group-holder'>Now it is empty</p>
)}
</div>
);
}, [mounted, groupInfo.updateTime, range, variableConfig, step]);
return (
<div className='n9e-dashboard-group'>
<Collapse defaultActiveKey={['0']}>
<Panel header={<span className='panel-title'>{groupInfo.name}</span>} key='0' extra={generateRightButton()}>
{renderCharts()}
</Panel>
</Collapse>
</div>
);
}
Example #15
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 #16
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 #17
Source File: BorderRadius.tsx From yugong with MIT License | 4 votes |
BorderRadius: React.FC<Props> = ({
unit,
onChange,
defaultData,
...other
}) => {
const [locked, setLocked] = useState(false);
const [topLeft, setTopLeft] = useState<UnitType>();
const [topRight, setTopRight] = useState<UnitType>();
const [bottomRight, setBottomRight] = useState<UnitType>();
const [bottomLeft, setBottomLeft] = useState<UnitType>();
useEffect(() => {
if (Array.isArray(defaultData)) {
setTopLeft(defaultData[0]);
setTopRight(defaultData[1]);
setBottomRight(defaultData[2]);
setBottomLeft(defaultData[3]);
}
}, [defaultData]);
const toggleLocked = useCallback(() => {
if (!locked) {
setTopLeft(topLeft);
setTopRight(topLeft);
setBottomRight(topLeft);
setBottomLeft(topLeft);
}
setLocked(!locked);
if (onChange instanceof Function) {
onChange([topLeft, topLeft, topLeft, topLeft]);
}
}, [locked, onChange, topLeft]);
const onChangeAll = useCallback((value: UnitType) => {
setTopLeft(value);
setTopRight(value);
setBottomRight(value);
setBottomLeft(value);
}, []);
const updateChange = useCallback((value: (UnitType | undefined)[]) => {
if (onChange instanceof Function) {
onChange(value)
}
}, [onChange])
const onChangeData = useCallback(
(index: number) => (value: UnitType) => {
if (locked) {
onChangeAll(value);
updateChange([value, value, value, value]);
return;
}
switch (index) {
case 0:
setTopLeft(value);
updateChange([value, topRight, bottomRight, bottomLeft]);
break;
case 1:
setTopRight(value);
updateChange([topLeft, value, bottomRight, bottomLeft]);
break;
case 2:
setBottomRight(value);
updateChange([topLeft, topRight, value, bottomLeft]);
break;
case 3:
setBottomLeft(value);
updateChange([topLeft, topRight, bottomRight, value]);
break;
default:
break;
}
},
[bottomLeft, bottomRight, locked, onChangeAll, topLeft, topRight, updateChange]
);
return (
<Row className={s.row}>
<Col span={11}>
<Row>
<Col span={4} className={classNames(s.icon, s.alignright)}>
<RadiusUpleftOutlined />
</Col>
<Col span={20}>
<UnitInput
min={0}
span={{ wrapper: 24 }}
defaultValue={topLeft}
onChange={onChangeData(0)}
/>
</Col>
</Row>
<Row>
<Col span={4} className={classNames(s.icon, s.alignright)}>
<RadiusBottomleftOutlined />
</Col>
<Col span={20}>
<UnitInput
min={0}
span={{ wrapper: 24 }}
defaultValue={bottomLeft}
onChange={onChangeData(3)}
/>
</Col>
</Row>
</Col>
<Col span={2} className={s.middle}>
<LinkOutlined
onClick={toggleLocked}
className={locked ? s.locked : undefined}
/>
</Col>
<Col span={11}>
<Row className={s.row}>
<Col span={20}>
<UnitInput
min={0}
span={{ wrapper: 24 }}
defaultValue={topRight}
onChange={onChangeData(1)}
/>
</Col>
<Col span={4} className={s.icon}>
<RadiusUprightOutlined />
</Col>
</Row>
<Row className={s.row}>
<Col span={20}>
<UnitInput
min={0}
span={{ wrapper: 24 }}
defaultValue={bottomRight}
onChange={onChangeData(2)}
/>
</Col>
<Col span={4} className={s.icon}>
<RadiusBottomrightOutlined />
</Col>
</Row>
</Col>
</Row>
);
}
Example #18
Source File: Icon.tsx From html2sketch with MIT License | 4 votes |
IconSymbol: FC = () => {
return (
<Row>
{/*<CaretUpOutlined*/}
{/* className="icon"*/}
{/* symbolName={'1.General/2.Icons/1.CaretUpOutlined'}*/}
{/*/>*/}
{/* className="icon"*/}
{/* symbolName={'1.General/2.Icons/2.MailOutlined'}*/}
{/*/>*/}
{/*<StepBackwardOutlined*/}
{/* className="icon"*/}
{/* symbolName={'1.General/2.Icons/2.StepBackwardOutlined'}*/}
{/*/>*/}
{/*<StepForwardOutlined*/}
{/* className="icon"*/}
{/* symbolName={'1.General/2.Icons/2.StepBackwardOutlined'}*/}
{/*/>*/}
<StepForwardOutlined />
<ShrinkOutlined />
<ArrowsAltOutlined />
<DownOutlined />
<UpOutlined />
<LeftOutlined />
<RightOutlined />
<CaretUpOutlined />
<CaretDownOutlined />
<CaretLeftOutlined />
<CaretRightOutlined />
<VerticalAlignTopOutlined />
<RollbackOutlined />
<FastBackwardOutlined />
<FastForwardOutlined />
<DoubleRightOutlined />
<DoubleLeftOutlined />
<VerticalLeftOutlined />
<VerticalRightOutlined />
<VerticalAlignMiddleOutlined />
<VerticalAlignBottomOutlined />
<ForwardOutlined />
<BackwardOutlined />
<EnterOutlined />
<RetweetOutlined />
<SwapOutlined />
<SwapLeftOutlined />
<SwapRightOutlined />
<ArrowUpOutlined />
<ArrowDownOutlined />
<ArrowLeftOutlined />
<ArrowRightOutlined />
<LoginOutlined />
<LogoutOutlined />
<MenuFoldOutlined />
<MenuUnfoldOutlined />
<BorderBottomOutlined />
<BorderHorizontalOutlined />
<BorderInnerOutlined />
<BorderOuterOutlined />
<BorderLeftOutlined />
<BorderRightOutlined />
<BorderTopOutlined />
<BorderVerticleOutlined />
<PicCenterOutlined />
<PicLeftOutlined />
<PicRightOutlined />
<RadiusBottomleftOutlined />
<RadiusBottomrightOutlined />
<RadiusUpleftOutlined />
<RadiusUprightOutlined />
<FullscreenOutlined />
<FullscreenExitOutlined />
<QuestionOutlined />
<PauseOutlined />
<MinusOutlined />
<PauseCircleOutlined />
<InfoOutlined />
<CloseOutlined />
<ExclamationOutlined />
<CheckOutlined />
<WarningOutlined />
<IssuesCloseOutlined />
<StopOutlined />
<EditOutlined />
<CopyOutlined />
<ScissorOutlined />
<DeleteOutlined />
<SnippetsOutlined />
<DiffOutlined />
<HighlightOutlined />
<AlignCenterOutlined />
<AlignLeftOutlined />
<AlignRightOutlined />
<BgColorsOutlined />
<BoldOutlined />
<ItalicOutlined />
<UnderlineOutlined />
<StrikethroughOutlined />
<RedoOutlined />
<UndoOutlined />
<ZoomInOutlined />
<ZoomOutOutlined />
<FontColorsOutlined />
<FontSizeOutlined />
<LineHeightOutlined />
<SortAscendingOutlined />
<SortDescendingOutlined />
<DragOutlined />
<OrderedListOutlined />
<UnorderedListOutlined />
<RadiusSettingOutlined />
<ColumnWidthOutlined />
<ColumnHeightOutlined />
<AreaChartOutlined />
<PieChartOutlined />
<BarChartOutlined />
<DotChartOutlined />
<LineChartOutlined />
<RadarChartOutlined />
<HeatMapOutlined />
<FallOutlined />
<RiseOutlined />
<StockOutlined />
<BoxPlotOutlined />
<FundOutlined />
<SlidersOutlined />
<AndroidOutlined />
<AppleOutlined />
<WindowsOutlined />
<IeOutlined />
<ChromeOutlined />
<GithubOutlined />
<AliwangwangOutlined />
<DingdingOutlined />
<WeiboSquareOutlined />
<WeiboCircleOutlined />
<TaobaoCircleOutlined />
<Html5Outlined />
<WeiboOutlined />
<TwitterOutlined />
<WechatOutlined />
<AlipayCircleOutlined />
<TaobaoOutlined />
<SkypeOutlined />
<FacebookOutlined />
<CodepenOutlined />
<CodeSandboxOutlined />
<AmazonOutlined />
<GoogleOutlined />
<AlipayOutlined />
<AntDesignOutlined />
<AntCloudOutlined />
<ZhihuOutlined />
<SlackOutlined />
<SlackSquareOutlined />
<BehanceSquareOutlined />
<DribbbleOutlined />
<DribbbleSquareOutlined />
<InstagramOutlined />
<YuqueOutlined />
<AlibabaOutlined />
<YahooOutlined />
<RedditOutlined />
<SketchOutlined />
<AccountBookOutlined />
<AlertOutlined />
<ApartmentOutlined />
<ApiOutlined />
<QqOutlined />
<MediumWorkmarkOutlined />
<GitlabOutlined />
<MediumOutlined />
<GooglePlusOutlined />
<AppstoreAddOutlined />
<AppstoreOutlined />
<AudioOutlined />
<AudioMutedOutlined />
<AuditOutlined />
<BankOutlined />
<BarcodeOutlined />
<BarsOutlined />
<BellOutlined />
<BlockOutlined />
<BookOutlined />
<BorderOutlined />
<BranchesOutlined />
<BuildOutlined />
<BulbOutlined />
<CalculatorOutlined />
<CalendarOutlined />
<CameraOutlined />
<CarOutlined />
<CarryOutOutlined />
<CiCircleOutlined />
<CiOutlined />
<CloudOutlined />
<ClearOutlined />
<ClusterOutlined />
<CodeOutlined />
<CoffeeOutlined />
<CompassOutlined />
<CompressOutlined />
<ContactsOutlined />
<ContainerOutlined />
<ControlOutlined />
<CopyrightCircleOutlined />
<CopyrightOutlined />
<CreditCardOutlined />
<CrownOutlined />
<CustomerServiceOutlined />
<DashboardOutlined />
<DatabaseOutlined />
<DeleteColumnOutlined />
<DeleteRowOutlined />
<DisconnectOutlined />
<DislikeOutlined />
<DollarCircleOutlined />
<DollarOutlined />
<DownloadOutlined />
<EllipsisOutlined />
<EnvironmentOutlined />
<EuroCircleOutlined />
<EuroOutlined />
<ExceptionOutlined />
<ExpandAltOutlined />
<ExpandOutlined />
<ExperimentOutlined />
<ExportOutlined />
<EyeOutlined />
<FieldBinaryOutlined />
<FieldNumberOutlined />
<FieldStringOutlined />
<DesktopOutlined />
<DingtalkOutlined />
<FileAddOutlined />
<FileDoneOutlined />
<FileExcelOutlined />
<FileExclamationOutlined />
<FileOutlined />
<FileImageOutlined />
<FileJpgOutlined />
<FileMarkdownOutlined />
<FilePdfOutlined />
<FilePptOutlined />
<FileProtectOutlined />
<FileSearchOutlined />
<FileSyncOutlined />
<FileTextOutlined />
<FileUnknownOutlined />
<FileWordOutlined />
<FilterOutlined />
<FireOutlined />
<FlagOutlined />
<FolderAddOutlined />
<FolderOutlined />
<FolderOpenOutlined />
<ForkOutlined />
<FormatPainterOutlined />
<FrownOutlined />
<FunctionOutlined />
<FunnelPlotOutlined />
<GatewayOutlined />
<GifOutlined />
<GiftOutlined />
<GlobalOutlined />
<GoldOutlined />
<GroupOutlined />
<HddOutlined />
<HeartOutlined />
<HistoryOutlined />
<HomeOutlined />
<HourglassOutlined />
<IdcardOutlined />
<ImportOutlined />
<InboxOutlined />
<InsertRowAboveOutlined />
<InsertRowBelowOutlined />
<InsertRowLeftOutlined />
<InsertRowRightOutlined />
<InsuranceOutlined />
<InteractionOutlined />
<KeyOutlined />
<LaptopOutlined />
<LayoutOutlined />
<LikeOutlined />
<LineOutlined />
<LinkOutlined />
<Loading3QuartersOutlined />
<LoadingOutlined />
<LockOutlined />
<MailOutlined />
<ManOutlined />
<MedicineBoxOutlined />
<MehOutlined />
<MenuOutlined />
<MergeCellsOutlined />
<MessageOutlined />
<MobileOutlined />
<MoneyCollectOutlined />
<MonitorOutlined />
<MoreOutlined />
<NodeCollapseOutlined />
<NodeExpandOutlined />
<NodeIndexOutlined />
<NotificationOutlined />
<NumberOutlined />
<PaperClipOutlined />
<PartitionOutlined />
<PayCircleOutlined />
<PercentageOutlined />
<PhoneOutlined />
<PictureOutlined />
<PoundCircleOutlined />
<PoundOutlined />
<PoweroffOutlined />
<PrinterOutlined />
<ProfileOutlined />
<ProjectOutlined />
<PropertySafetyOutlined />
<PullRequestOutlined />
<PushpinOutlined />
<QrcodeOutlined />
<ReadOutlined />
<ReconciliationOutlined />
<RedEnvelopeOutlined />
<ReloadOutlined />
<RestOutlined />
<RobotOutlined />
<RocketOutlined />
<SafetyCertificateOutlined />
<SafetyOutlined />
<ScanOutlined />
<ScheduleOutlined />
<SearchOutlined />
<SecurityScanOutlined />
<SelectOutlined />
<SendOutlined />
<SettingOutlined />
<ShakeOutlined />
<ShareAltOutlined />
<ShopOutlined />
<ShoppingCartOutlined />
<ShoppingOutlined />
<SisternodeOutlined />
<SkinOutlined />
<SmileOutlined />
<SolutionOutlined />
<SoundOutlined />
<SplitCellsOutlined />
<StarOutlined />
<SubnodeOutlined />
<SyncOutlined />
<TableOutlined />
<TabletOutlined />
<TagOutlined />
<TagsOutlined />
<TeamOutlined />
<ThunderboltOutlined />
<ToTopOutlined />
<ToolOutlined />
<TrademarkCircleOutlined />
<TrademarkOutlined />
<TransactionOutlined />
<TrophyOutlined />
<UngroupOutlined />
<UnlockOutlined />
<UploadOutlined />
<UsbOutlined />
<UserAddOutlined />
<UserDeleteOutlined />
<UserOutlined />
<UserSwitchOutlined />
<UsergroupAddOutlined />
<UsergroupDeleteOutlined />
<VideoCameraOutlined />
<WalletOutlined />
<WifiOutlined />
<BorderlessTableOutlined />
<WomanOutlined />
<BehanceOutlined />
<DropboxOutlined />
<DeploymentUnitOutlined />
<UpCircleOutlined />
<DownCircleOutlined />
<LeftCircleOutlined />
<RightCircleOutlined />
<UpSquareOutlined />
<DownSquareOutlined />
<LeftSquareOutlined />
<RightSquareOutlined />
<PlayCircleOutlined />
<QuestionCircleOutlined />
<PlusCircleOutlined />
<PlusSquareOutlined />
<MinusSquareOutlined />
<MinusCircleOutlined />
<InfoCircleOutlined />
<ExclamationCircleOutlined />
<CloseCircleOutlined />
<CloseSquareOutlined />
<CheckCircleOutlined />
<CheckSquareOutlined />
<ClockCircleOutlined />
<FormOutlined />
<DashOutlined />
<SmallDashOutlined />
<YoutubeOutlined />
<CodepenCircleOutlined />
<AliyunOutlined />
<PlusOutlined />
<LinkedinOutlined />
<AimOutlined />
<BugOutlined />
<CloudDownloadOutlined />
<CloudServerOutlined />
<CloudSyncOutlined />
<CloudUploadOutlined />
<CommentOutlined />
<ConsoleSqlOutlined />
<EyeInvisibleOutlined />
<FileGifOutlined />
<DeliveredProcedureOutlined />
<FieldTimeOutlined />
<FileZipOutlined />
<FolderViewOutlined />
<FundProjectionScreenOutlined />
<FundViewOutlined />
<MacCommandOutlined />
<PlaySquareOutlined />
<OneToOneOutlined />
<RotateLeftOutlined />
<RotateRightOutlined />
<SaveOutlined />
<SwitcherOutlined />
<TranslationOutlined />
<VerifiedOutlined />
<VideoCameraAddOutlined />
<WhatsAppOutlined />
{/*</Col>*/}
</Row>
);
}
Example #19
Source File: commandPaletteLogic.ts From posthog-foss with MIT License | 4 votes |
commandPaletteLogic = kea<
commandPaletteLogicType<
Command,
CommandFlow,
CommandRegistrations,
CommandResult,
CommandResultDisplayable,
RegExpCommandPairs
>
>({
path: ['lib', 'components', 'CommandPalette', 'commandPaletteLogic'],
connect: {
actions: [personalAPIKeysLogic, ['createKey']],
values: [teamLogic, ['currentTeam'], userLogic, ['user']],
logic: [preflightLogic], // used in afterMount, which does not auto-connect
},
actions: {
hidePalette: true,
showPalette: true,
togglePalette: true,
setInput: (input: string) => ({ input }),
onArrowUp: true,
onArrowDown: (maxIndex: number) => ({ maxIndex }),
onMouseEnterResult: (index: number) => ({ index }),
onMouseLeaveResult: true,
executeResult: (result: CommandResult) => ({ result }),
activateFlow: (flow: CommandFlow | null) => ({ flow }),
backFlow: true,
registerCommand: (command: Command) => ({ command }),
deregisterCommand: (commandKey: string) => ({ commandKey }),
setCustomCommand: (commandKey: string) => ({ commandKey }),
deregisterScope: (scope: string) => ({ scope }),
},
reducers: {
isPaletteShown: [
false,
{
hidePalette: () => false,
showPalette: () => true,
togglePalette: (previousState) => !previousState,
},
],
keyboardResultIndex: [
0,
{
setInput: () => 0,
executeResult: () => 0,
activateFlow: () => 0,
backFlow: () => 0,
onArrowUp: (previousIndex) => (previousIndex > 0 ? previousIndex - 1 : 0),
onArrowDown: (previousIndex, { maxIndex }) => (previousIndex < maxIndex ? previousIndex + 1 : maxIndex),
},
],
hoverResultIndex: [
null as number | null,
{
activateFlow: () => null,
backFlow: () => null,
onMouseEnterResult: (_, { index }) => index,
onMouseLeaveResult: () => null,
onArrowUp: () => null,
onArrowDown: () => null,
},
],
input: [
'',
{
setInput: (_, { input }) => input,
activateFlow: () => '',
backFlow: () => '',
executeResult: () => '',
},
],
activeFlow: [
null as CommandFlow | null,
{
activateFlow: (currentFlow, { flow }) =>
flow ? { ...flow, scope: flow.scope ?? currentFlow?.scope, previousFlow: currentFlow } : null,
backFlow: (currentFlow) => currentFlow?.previousFlow ?? null,
},
],
rawCommandRegistrations: [
{} as CommandRegistrations,
{
registerCommand: (commands, { command }) => {
return { ...commands, [command.key]: command }
},
deregisterCommand: (commands, { commandKey }) => {
const { [commandKey]: _, ...cleanedCommands } = commands // eslint-disable-line
return cleanedCommands
},
},
],
},
listeners: ({ actions, values }) => ({
showPalette: () => {
posthog.capture('palette shown', { isMobile: isMobile() })
},
togglePalette: () => {
if (values.isPaletteShown) {
posthog.capture('palette shown', { isMobile: isMobile() })
}
},
executeResult: ({ result }: { result: CommandResult }) => {
if (result.executor === true) {
actions.activateFlow(null)
actions.hidePalette()
} else {
const possibleFlow = result.executor?.() || null
actions.activateFlow(possibleFlow)
if (!possibleFlow) {
actions.hidePalette()
}
}
// Capture command execution, without useless data
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { icon, index, ...cleanedResult }: Record<string, any> = result
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const { resolver, ...cleanedCommand } = cleanedResult.source
cleanedResult.source = cleanedCommand
cleanedResult.isMobile = isMobile()
posthog.capture('palette command executed', cleanedResult)
},
deregisterScope: ({ scope }) => {
for (const command of Object.values(values.commandRegistrations)) {
if (command.scope === scope) {
actions.deregisterCommand(command.key)
}
}
},
setInput: async ({ input }, breakpoint) => {
await breakpoint(300)
if (input.length > 8) {
const response = await api.get('api/person/?key_identifier=' + encodeURIComponent(input))
const person = response.results[0]
if (person) {
actions.registerCommand({
key: `person-${person.distinct_ids[0]}`,
resolver: [
{
icon: UserOutlined,
display: `View person ${input}`,
executor: () => {
const { push } = router.actions
push(urls.person(person.distinct_ids[0]))
},
},
],
scope: GLOBAL_COMMAND_SCOPE,
})
}
}
},
}),
selectors: {
isSqueak: [
(selectors) => [selectors.input],
(input: string) => {
return input.trim().toLowerCase() === 'squeak'
},
],
activeResultIndex: [
(selectors) => [selectors.keyboardResultIndex, selectors.hoverResultIndex],
(keyboardResultIndex: number, hoverResultIndex: number | null) => {
return hoverResultIndex ?? keyboardResultIndex
},
],
commandRegistrations: [
(selectors) => [
selectors.rawCommandRegistrations,
dashboardsModel.selectors.nameSortedDashboards,
teamLogic.selectors.currentTeam,
],
(rawCommandRegistrations: CommandRegistrations, dashboards: DashboardType[]): CommandRegistrations => ({
...rawCommandRegistrations,
custom_dashboards: {
key: 'custom_dashboards',
resolver: dashboards.map((dashboard: DashboardType) => ({
key: `dashboard_${dashboard.id}`,
icon: LineChartOutlined,
display: `Go to Dashboard: ${dashboard.name}`,
executor: () => {
const { push } = router.actions
push(urls.dashboard(dashboard.id))
},
})),
scope: GLOBAL_COMMAND_SCOPE,
},
}),
],
regexpCommandPairs: [
(selectors) => [selectors.commandRegistrations],
(commandRegistrations: CommandRegistrations) => {
const array: RegExpCommandPairs = []
for (const command of Object.values(commandRegistrations)) {
if (command.prefixes) {
array.push([new RegExp(`^\\s*(${command.prefixes.join('|')})(?:\\s+(.*)|$)`, 'i'), command])
} else {
array.push([null, command])
}
}
return array
},
],
commandSearchResults: [
(selectors) => [
selectors.isPaletteShown,
selectors.regexpCommandPairs,
selectors.input,
selectors.activeFlow,
selectors.isSqueak,
],
(
isPaletteShown: boolean,
regexpCommandPairs: RegExpCommandPairs,
argument: string,
activeFlow: CommandFlow | null,
isSqueak: boolean
) => {
if (!isPaletteShown || isSqueak) {
return []
}
if (activeFlow) {
return resolveCommand(activeFlow, argument)
}
let directResults: CommandResult[] = []
let prefixedResults: CommandResult[] = []
for (const [regexp, command] of regexpCommandPairs) {
if (regexp) {
const match = argument.match(regexp)
if (match && match[1]) {
prefixedResults = [...prefixedResults, ...resolveCommand(command, match[2], match[1])]
}
}
directResults = [...directResults, ...resolveCommand(command, argument)]
}
const allResults = directResults.concat(prefixedResults)
let fusableResults: CommandResult[] = []
let guaranteedResults: CommandResult[] = []
for (const result of allResults) {
if (result.guarantee) {
guaranteedResults.push(result)
} else {
fusableResults.push(result)
}
}
fusableResults = uniqueBy(fusableResults, (result) => result.display)
guaranteedResults = uniqueBy(guaranteedResults, (result) => result.display)
const fusedResults = argument
? new Fuse(fusableResults, {
keys: ['display', 'synonyms'],
})
.search(argument)
.slice(0, RESULTS_MAX)
.map((result) => result.item)
: sample(fusableResults, RESULTS_MAX - guaranteedResults.length)
return guaranteedResults.concat(fusedResults)
},
],
commandSearchResultsGrouped: [
(selectors) => [selectors.commandSearchResults, selectors.activeFlow],
(commandSearchResults: CommandResult[], activeFlow: CommandFlow | null) => {
const resultsGrouped: {
[scope: string]: CommandResult[]
} = {}
if (activeFlow) {
resultsGrouped[activeFlow.scope ?? '?'] = []
}
for (const result of commandSearchResults) {
const scope: string = result.source.scope ?? '?'
if (!(scope in resultsGrouped)) {
resultsGrouped[scope] = []
} // Ensure there's an array to push to
resultsGrouped[scope].push({ ...result })
}
let rollingGroupIndex = 0
let rollingResultIndex = 0
const resultsGroupedInOrder: [string, CommandResultDisplayable[]][] = []
for (const [group, results] of Object.entries(resultsGrouped)) {
resultsGroupedInOrder.push([group, []])
for (const result of results) {
resultsGroupedInOrder[rollingGroupIndex][1].push({ ...result, index: rollingResultIndex++ })
}
rollingGroupIndex++
}
return resultsGroupedInOrder
},
],
},
events: ({ actions }) => ({
afterMount: () => {
const { push } = router.actions
const goTo: Command = {
key: 'go-to',
scope: GLOBAL_COMMAND_SCOPE,
prefixes: ['open', 'visit'],
resolver: [
{
icon: FundOutlined,
display: 'Go to Dashboards',
executor: () => {
push(urls.dashboards())
},
},
{
icon: RiseOutlined,
display: 'Go to Insights',
executor: () => {
push(urls.savedInsights())
},
},
{
icon: RiseOutlined,
display: 'Go to Trends',
executor: () => {
// TODO: Don't reset insight on change
push(urls.insightNew({ insight: InsightType.TRENDS }))
},
},
{
icon: FunnelPlotOutlined,
display: 'Go to Funnels',
executor: () => {
// TODO: Don't reset insight on change
push(urls.insightNew({ insight: InsightType.FUNNELS }))
},
},
{
icon: GatewayOutlined,
display: 'Go to Retention',
executor: () => {
// TODO: Don't reset insight on change
push(urls.insightNew({ insight: InsightType.RETENTION }))
},
},
{
icon: InteractionOutlined,
display: 'Go to Paths',
executor: () => {
// TODO: Don't reset insight on change
push(urls.insightNew({ insight: InsightType.PATHS }))
},
},
{
icon: ContainerOutlined,
display: 'Go to Events',
executor: () => {
push(urls.events())
},
},
{
icon: AimOutlined,
display: 'Go to Actions',
executor: () => {
push(urls.actions())
},
},
{
icon: UserOutlined,
display: 'Go to Persons',
synonyms: ['people'],
executor: () => {
push(urls.persons())
},
},
{
icon: UsergroupAddOutlined,
display: 'Go to Cohorts',
executor: () => {
push(urls.cohorts())
},
},
{
icon: FlagOutlined,
display: 'Go to Feature Flags',
synonyms: ['feature flags', 'a/b tests'],
executor: () => {
push(urls.featureFlags())
},
},
{
icon: MessageOutlined,
display: 'Go to Annotations',
executor: () => {
push(urls.annotations())
},
},
{
icon: TeamOutlined,
display: 'Go to Team members',
synonyms: ['organization', 'members', 'invites', 'teammates'],
executor: () => {
push(urls.organizationSettings())
},
},
{
icon: ProjectOutlined,
display: 'Go to Project settings',
executor: () => {
push(urls.projectSettings())
},
},
{
icon: SmileOutlined,
display: 'Go to My settings',
synonyms: ['account'],
executor: () => {
push(urls.mySettings())
},
},
{
icon: ApiOutlined,
display: 'Go to Plugins',
synonyms: ['integrations'],
executor: () => {
push(urls.plugins())
},
},
{
icon: DatabaseOutlined,
display: 'Go to System status page',
synonyms: ['redis', 'celery', 'django', 'postgres', 'backend', 'service', 'online'],
executor: () => {
push(urls.systemStatus())
},
},
{
icon: PlusOutlined,
display: 'Create action',
executor: () => {
push(urls.createAction())
},
},
{
icon: LogoutOutlined,
display: 'Log out',
executor: () => {
userLogic.actions.logout()
},
},
],
}
const debugClickhouseQueries: Command = {
key: 'debug-clickhouse-queries',
scope: GLOBAL_COMMAND_SCOPE,
resolver:
userLogic.values.user?.is_staff ||
userLogic.values.user?.is_impersonated ||
preflightLogic.values.preflight?.is_debug ||
preflightLogic.values.preflight?.instance_preferences?.debug_queries
? {
icon: PlusOutlined,
display: 'Debug queries (ClickHouse)',
executor: () => {
debugCHQueries()
},
}
: [],
}
const calculator: Command = {
key: 'calculator',
scope: GLOBAL_COMMAND_SCOPE,
resolver: (argument) => {
// don't try evaluating if there's no argument or if it's a plain number already
if (!argument || !isNaN(+argument)) {
return null
}
try {
const result = +Parser.evaluate(argument)
return isNaN(result)
? null
: {
icon: CalculatorOutlined,
display: `= ${result}`,
guarantee: true,
executor: () => {
copyToClipboard(result.toString(), 'calculation result')
},
}
} catch {
return null
}
},
}
const openUrls: Command = {
key: 'open-urls',
scope: GLOBAL_COMMAND_SCOPE,
prefixes: ['open', 'visit'],
resolver: (argument) => {
const results: CommandResultTemplate[] = (teamLogic.values.currentTeam?.app_urls ?? []).map(
(url: string) => ({
icon: LinkOutlined,
display: `Open ${url}`,
synonyms: [`Visit ${url}`],
executor: () => {
open(url)
},
})
)
if (argument && isURL(argument)) {
results.push({
icon: LinkOutlined,
display: `Open ${argument}`,
synonyms: [`Visit ${argument}`],
executor: () => {
open(argument)
},
})
}
results.push({
icon: LinkOutlined,
display: 'Open PostHog Docs',
synonyms: ['technical documentation'],
executor: () => {
open('https://posthog.com/docs')
},
})
return results
},
}
const createPersonalApiKey: Command = {
key: 'create-personal-api-key',
scope: GLOBAL_COMMAND_SCOPE,
resolver: {
icon: KeyOutlined,
display: 'Create Personal API Key',
executor: () => ({
instruction: 'Give your key a label',
icon: TagOutlined,
scope: 'Creating Personal API Key',
resolver: (argument) => {
if (argument?.length) {
return {
icon: KeyOutlined,
display: `Create Key "${argument}"`,
executor: () => {
personalAPIKeysLogic.actions.createKey(argument)
push(urls.mySettings(), {}, 'personal-api-keys')
},
}
}
return null
},
}),
},
}
const createDashboard: Command = {
key: 'create-dashboard',
scope: GLOBAL_COMMAND_SCOPE,
resolver: {
icon: FundOutlined,
display: 'Create Dashboard',
executor: () => ({
instruction: 'Name your new dashboard',
icon: TagOutlined,
scope: 'Creating Dashboard',
resolver: (argument) => {
if (argument?.length) {
return {
icon: FundOutlined,
display: `Create Dashboard "${argument}"`,
executor: () => {
dashboardsModel.actions.addDashboard({ name: argument, show: true })
},
}
}
return null
},
}),
},
}
const shareFeedback: Command = {
key: 'share-feedback',
scope: GLOBAL_COMMAND_SCOPE,
resolver: {
icon: CommentOutlined,
display: 'Share Feedback',
synonyms: ['send opinion', 'ask question', 'message posthog', 'github issue'],
executor: () => ({
scope: 'Sharing Feedback',
resolver: [
{
display: 'Send Message Directly to PostHog',
icon: CommentOutlined,
executor: () => ({
instruction: "What's on your mind?",
icon: CommentOutlined,
resolver: (argument) => ({
icon: SendOutlined,
display: 'Send',
executor: !argument?.length
? undefined
: () => {
posthog.capture('palette feedback', { message: argument })
return {
resolver: {
icon: CheckOutlined,
display: 'Message Sent!',
executor: true,
},
}
},
}),
}),
},
{
icon: VideoCameraOutlined,
display: 'Schedule Quick Call',
executor: () => {
open('https://calendly.com/posthog-feedback')
},
},
{
icon: ExclamationCircleOutlined,
display: 'Create GitHub Issue',
executor: () => {
open('https://github.com/PostHog/posthog/issues/new/choose')
},
},
],
}),
},
}
actions.registerCommand(goTo)
actions.registerCommand(openUrls)
actions.registerCommand(debugClickhouseQueries)
actions.registerCommand(calculator)
actions.registerCommand(createPersonalApiKey)
actions.registerCommand(createDashboard)
actions.registerCommand(shareFeedback)
},
beforeUnmount: () => {
actions.deregisterCommand('go-to')
actions.deregisterCommand('open-urls')
actions.deregisterCommand('debug-clickhouse-queries')
actions.deregisterCommand('calculator')
actions.deregisterCommand('create-personal-api-key')
actions.deregisterCommand('create-dashboard')
actions.deregisterCommand('share-feedback')
},
}),
})