@ant-design/icons#CheckOutlined TypeScript Examples
The following examples show how to use
@ant-design/icons#CheckOutlined.
You can vote up the ones you like or vote down the ones you don't like,
and go to the original project or source file by following the links above each example. You may check out the related API usage on the sidebar.
Example #1
Source File: index.tsx From nanolooker with MIT License | 6 votes |
LivePreference: React.FC<Props> = ({ isDetailed }) => {
const { t } = useTranslation();
const {
disableLiveTransactions,
setDisableLiveTransactions,
} = React.useContext(PreferencesContext);
return (
<Row>
<Col xs={isDetailed ? 24 : 18}>
<Text className={isDetailed ? "preference-detailed-title" : ""}>
{t("preferences.liveTransactions")}
</Text>
</Col>
{isDetailed ? (
<Col xs={18}>
<Text>
{t("preferences.liveTransactionsDetailed")}
</Text>
</Col>
) : null}
<Col xs={6} style={{ textAlign: "right" }}>
<Switch
checkedChildren={<CheckOutlined />}
unCheckedChildren={<CloseOutlined />}
onChange={(checked: boolean) => {
setDisableLiveTransactions(!checked);
}}
checked={!disableLiveTransactions}
/>
</Col>
</Row>
);
}
Example #2
Source File: index.tsx From nanolooker with MIT License | 6 votes |
ThemePreference: React.FC<Props> = ({ isDetailed }) => {
const { t } = useTranslation();
const { theme, setTheme } = React.useContext(PreferencesContext);
return (
<Row>
<Col xs={isDetailed ? 24 : 18}>
<Text className={isDetailed ? "preference-detailed-title" : ""}>
{t("preferences.darkMode")}
</Text>
</Col>
{isDetailed ? (
<Col xs={18}>
<Text>{t("preferences.darkModeDetailed")}</Text>
</Col>
) : null}
<Col xs={6} style={{ textAlign: "right" }}>
<Switch
checkedChildren={<CheckOutlined />}
unCheckedChildren={<CloseOutlined />}
onChange={(checked: boolean) => {
setTheme(checked ? Theme.DARK : Theme.LIGHT);
}}
checked={theme === Theme.DARK}
/>
</Col>
</Row>
);
}
Example #3
Source File: BasicUnControlledTabPanel.tsx From datart with Apache License 2.0 | 6 votes |
EditableTabHeader: FC<{
label: string;
rowKey: string;
editable?: Boolean;
onChange: (key: string, value: string) => void;
}> = memo(({ label, rowKey, editable = true, onChange }) => {
const [isEditing, setIsEditing] = useState(false);
const render = () => {
if (!editable) {
return <span>{label}</span>;
}
return isEditing ? (
<Search
enterButton={<CheckOutlined />}
size="small"
onSearch={value => {
if (!!value) {
setIsEditing(false);
onChange(rowKey, value);
}
}}
/>
) : (
<Space>
<Button
block
type="text"
icon={<EditOutlined />}
onClick={() => setIsEditing(true)}
></Button>
<span>{label}</span>
</Space>
);
};
return render();
})
Example #4
Source File: StudentBanner.tsx From office-hours with GNU General Public License v3.0 | 6 votes |
function QuestionDetailRow({ studentQuestion }: { studentQuestion: Question }) {
return (
<QuestionDetails>
<ColWithRightMargin flex="4 4">
<InfoHeader>question</InfoHeader>
<div>{studentQuestion.text}</div>
</ColWithRightMargin>
<Col flex="0.5 0.5 95px">
<InfoHeader>type</InfoHeader>
<div>{studentQuestion.questionType}</div>
</Col>
<Col flex="0 0 89px">
<InfoHeader>groupable</InfoHeader>
<div>
{studentQuestion.groupable ? <CheckOutlined /> : <CloseOutlined />}
</div>
</Col>
</QuestionDetails>
);
}
Example #5
Source File: PluginUpdateButton.tsx From posthog-foss with MIT License | 6 votes |
PluginUpdateButton = ({ updateStatus, pluginId, rearranging }: PluginUpdateButtonProps): JSX.Element => {
const { editPlugin, updatePlugin } = useActions(pluginsLogic)
const { pluginsUpdating } = useValues(pluginsLogic)
return (
<Button
type={updateStatus?.updated ? 'default' : 'primary'}
className="padding-under-500"
onClick={() => (updateStatus?.updated ? editPlugin(pluginId) : updatePlugin(pluginId))}
loading={pluginsUpdating.includes(pluginId)}
icon={updateStatus?.updated ? <CheckOutlined /> : <CloudDownloadOutlined />}
disabled={rearranging}
data-attr="plugin-update"
>
<span className="show-over-500">{updateStatus?.updated ? 'Updated' : 'Update'}</span>
</Button>
)
}
Example #6
Source File: UnControlledTableHeaderPanel.tsx From datart with Apache License 2.0 | 6 votes |
EditableLabel: FC<{
label: string;
editable?: Boolean;
onChange: (value: string) => void;
}> = memo(({ label, editable = true, onChange }) => {
const [isEditing, setIsEditing] = useState(false);
const render = () => {
if (!editable) {
return <span>{label}</span>;
}
return isEditing ? (
<Search
enterButton={<CheckOutlined />}
placeholder={label}
size="small"
onSearch={value => {
if (!!value) {
setIsEditing(false);
onChange(value);
}
}}
/>
) : (
<>
<span>{label}</span>
<Button
type="text"
size="small"
icon={<EditOutlined />}
onClick={() => setIsEditing(true)}
></Button>
</>
);
};
return <StyledEditableLabel>{render()}</StyledEditableLabel>;
})
Example #7
Source File: FunctionDebuggerStatusbar.tsx From next-basics with GNU General Public License v3.0 | 5 votes |
export function FunctionDebuggerStatusbar({
coverage,
testStats,
}: FunctionDebuggerStatusbarProps): React.ReactElement {
const coverageIsOk = coverage && coverage.status !== "failed";
const totalCoverage = useMemo(
() => (coverageIsOk ? getTotalCoverage(coverage) : null),
[coverageIsOk, coverage]
);
const coverageStats = useMemo(
() => (coverageIsOk ? getCoverageStats(coverage) : null),
[coverageIsOk, coverage]
);
return (
<div className={styles.debuggerStatusbar} data-override-theme="dark">
<div className={styles.coverage}>
{coverage == null ? (
<span>
<span className={styles.coverageIcon}>
<QuestionOutlined />
</span>
<span>Coverage: expired</span>
</span>
) : testStats?.failed > 0 ? (
<span className={styles.hasFailedTests}>
<span className={styles.coverageIcon}>
<WarningOutlined />
</span>
<span>
{testStats.failed}/{testStats.total} tests failed!
</span>
</span>
) : coverageIsOk ? (
<>
<span
className={
totalCoverage < 60
? styles.coverageLow
: totalCoverage < 90
? styles.coverageMedium
: totalCoverage < 100
? styles.coverageHigh
: styles.coverageFull
}
>
<span className={styles.coverageIcon}>
{totalCoverage < 100 ? <WarningOutlined /> : <CheckOutlined />}
</span>
<span>Coverage: {totalCoverage}%</span>
</span>
{Object.entries(coverageStats).map(
([type, { covered, total, percentage }]) => (
<span key={type} className={styles.subCoverage}>
<span>{upperFirst(type)}: </span>
<span>
{percentage}% ({covered}/{total})
</span>
</span>
)
)}
</>
) : (
<span className={styles.coverageFailed}>
<span className={styles.coverageIcon}>
<CloseOutlined />
</span>
<span>{(coverage as RawCoverageFailed).error}</span>
</span>
)}
</div>
</div>
);
}
Example #8
Source File: index.tsx From nanolooker with MIT License | 5 votes |
FilterTransactionsPreferences: React.FC<Props> = ({ isDetailed }) => {
const { t } = useTranslation();
const {
filterTransactions,
filterTransactionsRange,
setFilterTransactions,
} = React.useContext(PreferencesContext);
const range = `${
units.find(({ raw }) => raw === filterTransactionsRange[1])?.display
} - ${
units.find(({ raw }) => raw === filterTransactionsRange[0])?.display ||
t("pages.preferences.noLimit")
}`;
return (
<>
<Row>
<Col xs={18}>
<Text style={{ whiteSpace: "nowrap", paddingRight: "18px" }}>
{isEqual(filterTransactionsRange, DEFAULT_UNITS) ? (
t("preferences.filterTransactions")
) : (
<>
{t("preferences.filterTransactionsRange")}
<br />
<strong>{range}</strong>
</>
)}
</Text>
<br />
<Link to="/preferences" style={{ whiteSpace: "nowrap" }}>
{t("preferences.filterTransactionsRangeDetailed")}
</Link>
</Col>
<Col xs={6} style={{ textAlign: "right" }}>
<Switch
checkedChildren={<CheckOutlined />}
unCheckedChildren={<CloseOutlined />}
onChange={(checked: boolean) => {
setFilterTransactions(checked);
}}
checked={filterTransactions}
/>
</Col>
</Row>
</>
);
}
Example #9
Source File: index.tsx From nanolooker with MIT License | 5 votes |
NatriconsPreferences: React.FC<Props> = ({ isDetailed }) => {
const { t } = useTranslation();
const [account] = React.useState(
DEVELOPER_FUND_ACCOUNTS[
Math.floor(Math.random() * DEVELOPER_FUND_ACCOUNTS.length)
],
);
const { natricons, setNatricons } = React.useContext(PreferencesContext);
return (
<div style={{ display: "flex", alignItems: "flex-start" }}>
<Natricon
account={account}
style={{
margin: "-12px -6px -18px -18px ",
width: "80px",
height: "80px",
}}
/>
<Row style={{ width: "100%" }}>
<Col xs={isDetailed ? 24 : 18}>
<Text className={isDetailed ? "preference-detailed-title" : ""}>
{t("preferences.natricons")}
</Text>
</Col>
{isDetailed ? (
<Col xs={18}>
<Text>{t("preferences.natriconsDetailed")}</Text>
</Col>
) : null}
<Col xs={6} style={{ textAlign: "right" }}>
<Switch
checkedChildren={<CheckOutlined />}
unCheckedChildren={<CloseOutlined />}
onChange={(checked: boolean) => {
setNatricons(checked);
}}
checked={natricons}
/>
</Col>
</Row>
</div>
);
}
Example #10
Source File: ErrorCard.tsx From jitsu with MIT License | 5 votes |
ErrorCard: FC<ErrorCardProps> = ({
title,
icon,
error,
description,
descriptionWithContacts,
stackTrace,
className,
onReload,
}) => {
if (description === undefined && error !== undefined) {
description = error.message
}
if (stackTrace === undefined && error !== undefined) {
stackTrace = error.stack
}
return (
<Card bordered={false} className={cn(className, "max-h-full")}>
<Card.Meta
avatar={icon || <ExclamationCircleOutlined className={styles.icon} />}
title={title || "An Error Occured"}
description={
<>
<Fragment key="description">
{description !== undefined ? (
description
) : (
<span>
{descriptionWithContacts !== undefined ? (
<>
{descriptionWithContacts}
{descriptionWithContacts && <br />}
</>
) : (
<>
{"The application component crashed because of an internal error."}
<br />
</>
)}
{"Please, try to reload the page first and if the problem is still present contact us at"}{" "}
<Typography.Paragraph copyable={{ tooltips: false }} className="inline">
{"[email protected]"}
</Typography.Paragraph>{" "}
{"and our engineers will fix the problem asap."}
</span>
)}
</Fragment>
{stackTrace && (
<Collapse key="stack-trace" bordered={false} className={`mt-2 ${styles.stackTraceCard}`}>
<Collapse.Panel key={1} header="Error Stack Trace">
<div className="overflow-y-auto">
<Typography.Paragraph
copyable={{
text: stackTrace,
icon: [<CopyOutlined />, <CheckOutlined />],
}}
className={`flex flex-row ${styles.errorStackContainer}`}
>
<pre className="text-xs">{stackTrace}</pre>
</Typography.Paragraph>
</div>
</Collapse.Panel>
</Collapse>
)}
{onReload && (
<div key="reload-button" className="flex justify-center items-center mt-2">
<Button type="default" onClick={onReload} icon={<ReloadOutlined />}>{`Reload`}</Button>
</div>
)}
</>
}
/>
</Card>
)
}
Example #11
Source File: index.tsx From foodie with MIT License | 5 votes |
FollowButton: React.FC<IProps> = (props) => {
const [isFollowing, setIsFollowing] = useState(props.isFollowing);
const [isLoading, setLoading] = useState(false);
const didMount = useDidMount();
useEffect(() => {
setIsFollowing(props.isFollowing);
}, [props.isFollowing])
const dispatchFollow = async () => {
try {
setLoading(true);
if (isFollowing) {
const result = await unfollowUser(props.userID);
didMount && setIsFollowing(result.state);
} else {
const result = await followUser(props.userID);
didMount && setIsFollowing(result.state);
}
didMount && setLoading(false);
} catch (e) {
didMount && setLoading(false);
console.log(e);
}
};
return (
<button
className={`${isFollowing && 'hover:bg-gray-200 bg-indigo-100 !border !border-indigo-500 text-indigo-700 dark:bg-indigo-1100 dark:text-indigo-400 dark:hover:bg-indigo-900 dark:hover:text-white'} flex items-center ${props.size === 'sm' && '!py-2 !px-3 !text-sm'}`}
disabled={isLoading}
onClick={dispatchFollow}
>
{isFollowing ? <CheckOutlined /> : <UserAddOutlined />}
<span className={`${props.size === 'sm' && 'text-sm'}`}>
{isLoading
? 'Following'
: !isLoading && !isFollowing
? 'Follow'
: 'Following'}
</span>
</button>
);
}
Example #12
Source File: AggregationAction.tsx From datart with Apache License 2.0 | 5 votes |
AggregationAction: FC<{
config: ChartDataSectionField;
onConfigChange: (
config: ChartDataSectionField,
needRefresh?: boolean,
) => void;
mode?: 'menu';
}> = ({ config, onConfigChange, mode }) => {
const t = useI18NPrefix(`viz.common.enum.aggregateTypes`);
const actionNeedNewRequest = true;
const [aggregate, setAggregate] = useState(config?.aggregate);
const onChange = selectedValue => {
const newConfig = updateBy(config, draft => {
draft.aggregate = selectedValue;
});
setAggregate(selectedValue);
onConfigChange?.(newConfig, actionNeedNewRequest);
};
const renderOptions = mode => {
if (mode === 'menu') {
return (
<>
{AggregateFieldSubAggregateType[
ChartDataSectionFieldActionType.Aggregate
]?.map(agg => {
return (
<Menu.Item
key={agg}
eventKey={agg}
icon={aggregate === agg ? <CheckOutlined /> : ''}
onClick={() => onChange(agg)}
>
{t(agg)}
</Menu.Item>
);
})}
</>
);
}
return (
<Radio.Group onChange={e => onChange(e.target?.value)} value={aggregate}>
<Space direction="vertical">
{AggregateFieldSubAggregateType[
ChartDataSectionFieldActionType.Aggregate
]?.map(agg => {
return (
<Radio key={agg} value={agg}>
{t(agg)}
</Radio>
);
})}
</Space>
</Radio.Group>
);
};
return renderOptions(mode);
}
Example #13
Source File: InputConfirm.tsx From iot-center-v2 with MIT License | 5 votes |
InputConfirm: React.FC<TInputConfirmProps> = (props) => {
const {value, onValueChange, tooltip} = props
const [newValue, setNewValue] = useState('')
const [isEditing, setIsEditing] = useState(false)
const [loading, setLoading] = useState(false)
useEffect(() => {
setNewValue(value ?? '')
}, [value])
return (
<Row>
<Col flex="auto">
{!isEditing ? (
value ?? ''
) : (
<Tooltip title={tooltip ?? ''}>
<Input
value={newValue}
onChange={(e) => setNewValue(e.target.value)}
style={{width: '100%'}}
/>
</Tooltip>
)}
</Col>
<Col>
{!isEditing ? (
<Tooltip title={'Edit'} key="edit">
<Button
size="small"
type="text"
icon={<EditOutlined />}
onClick={() => setIsEditing(true)}
></Button>
</Tooltip>
) : (
<>
<Tooltip title={loading ? '' : 'Cancel'} color="red" key="cancel">
<Button
size="small"
type="text"
disabled={loading}
icon={<CloseOutlined />}
onClick={() => {
setIsEditing(false)
}}
danger
></Button>
</Tooltip>
<Tooltip title={loading ? '' : 'Save'} color="green">
<Button
size="small"
type="text"
disabled={loading}
loading={loading}
style={{color: 'green'}}
icon={<CheckOutlined />}
onClick={async () => {
try {
setLoading(true)
await (onValueChange ?? (() => undefined))(newValue)
} finally {
setIsEditing(false)
setLoading(false)
}
}}
></Button>
</Tooltip>
</>
)}
</Col>
</Row>
)
}
Example #14
Source File: CurrentGroupList.tsx From office-hours with GNU General Public License v3.0 | 5 votes |
export function CurrentGroupList({
group,
queueId,
courseId,
}: {
group?: QuestionGroup;
queueId: number;
courseId: number;
}): ReactElement {
return (
<div>
<Header>
<div>
<strong>{`${group?.creator.name}'s Group Session`}</strong>
<Description>
{group?.questions.map((q) => q.creator.name).join(", ")}
</Description>
</div>
<div>
<Tooltip title="Finish Helping">
<FinishHelpingButton
icon={<CheckOutlined />}
onClick={() => API.questions.resolveGroup(group?.id, queueId)}
data-cy="finish-helping-button"
/>
</Tooltip>
</div>
</Header>
{group?.questions.map((q) => (
<div key={q.id}>
<TAQueueDetailQuestion
courseId={courseId}
question={q}
queueId={queueId}
showName
showButtons
hasUnresolvedRephraseAlert={false}
/>
</div>
))}
</div>
);
}
Example #15
Source File: index.tsx From amiya with MIT License | 5 votes |
export default function PopoverEdit(props: IProps) {
const { onChange, children, title, options, hidden } = props
// 是否可见
const [visible, setVisible] = useState(false)
// 当前输入值
const [text, setText] = useState<number | undefined>()
// 单位
const [unit, setUnit] = useState<number | undefined>()
useEffect(() => {
if (visible === false) {
setText(undefined)
setUnit(undefined)
} else {
setUnitDefaultValue()
}
}, [visible])
useEffect(() => setUnitDefaultValue(), [options])
/** 设置单位默认值 */
const setUnitDefaultValue = () => {
if (options && options.length) {
setUnit(options[0].value)
}
}
/** 确定更改 */
const handleConfirm = () => {
setVisible(false)
if (onChange) {
onChange(text, unit)
}
}
if (hidden === true) {
return <div>{children}</div>
}
return (
<Popover
visible={visible}
onVisibleChange={setVisible}
title={title}
trigger="click"
content={
<Space>
<InputNumber
value={text}
onChange={(value: number) => setText(value)}
addonBefore={options ? <AySelect value={unit} onChange={setUnit} options={options} /> : undefined}
/>
<AyButton type="primary" icon={<CheckOutlined />} onClick={handleConfirm} />
</Space>
}
>
{children}
<AyButton type="link">批量</AyButton>
</Popover>
)
}
Example #16
Source File: DateLevelMenuItems.tsx From datart with Apache License 2.0 | 4 votes |
DateLevelMenuItems = memo(
({ availableSourceFunctions, config, onChange }: DateLevelMenuItemsProps) => {
const t = useI18NPrefix(`viz.workbench.dataview`);
const handleChangeFn = useCallback(
selectedConfig => {
/**
* If the current category is DateLevelComputedField
*/
if (
config.category === ChartDataViewFieldCategory.DateLevelComputedField
) {
/**
* If default is selected
*/
if (selectedConfig.category === ChartDataViewFieldCategory.Field) {
return onChange(
updateBy(config, draft => {
delete draft.expression;
delete draft.field;
draft.category = selectedConfig.category;
draft.colName = selectedConfig.colName;
draft[RUNTIME_DATE_LEVEL_KEY] = null;
}),
);
}
return onChange({
...config,
colName: `${config.field}(${selectedConfig.colName})`,
expression: selectedConfig.expression,
[RUNTIME_DATE_LEVEL_KEY]: null,
});
} else {
/**
* If the current category is Field, only the selected category is judged to be DateLevelComputedField
*/
if (
selectedConfig.category ===
ChartDataViewFieldCategory.DateLevelComputedField
) {
return onChange(
updateBy(config, draft => {
draft.expression = selectedConfig.expression;
draft.field = config.colName;
draft.category =
ChartDataViewFieldCategory.DateLevelComputedField;
draft.colName = `${draft.colName}(${selectedConfig.colName})`;
draft[RUNTIME_DATE_LEVEL_KEY] = null;
}),
);
}
}
},
[config, onChange],
);
return (
<>
<Menu.Item
icon={!config.expression ? <CheckOutlined /> : ''}
onClick={() => {
config.field &&
handleChangeFn({
category: ChartDataViewFieldCategory.Field,
colName: config.field,
});
}}
>
{t('default')}
</Menu.Item>
{DATE_LEVELS.map(item => {
if (availableSourceFunctions?.includes(item.expression)) {
const colName = t(item.expression);
const expression = `${item.expression}(${
config.category === ChartDataViewFieldCategory.Field
? config.colName
: config.field
})`;
return (
<Menu.Item
key={expression}
eventKey={expression}
icon={config.expression === expression ? <CheckOutlined /> : ''}
onClick={() =>
handleChangeFn({
category: ChartDataViewFieldCategory.DateLevelComputedField,
colName,
expression,
})
}
>
{colName}
</Menu.Item>
);
}
return null;
})}
</>
);
},
)
Example #17
Source File: index.tsx From nanolooker with MIT License | 4 votes |
ConnectionPreferences: React.FC<Props> = ({ isDetailed }) => {
const { t } = useTranslation();
const {
rpcDomain,
setRpcDomain,
websocketDomain,
setWebsocketDomain,
} = React.useContext(PreferencesContext);
const [isEnabled, setIsEnabled] = React.useState(!!rpcDomain);
const {
control,
handleSubmit,
setValue,
getValues,
formState: { errors, isValid },
} = useForm({
defaultValues: {
rpcDomain: rpcDomain || "",
websocketDomain: websocketDomain || "",
},
mode: "onChange",
});
const onSubmit = async ({
rpcDomain,
websocketDomain,
}: {
rpcDomain?: string;
websocketDomain?: string;
}) => {
if (rpcDomain) {
setRpcDomain(rpcDomain);
}
if (websocketDomain) {
setWebsocketDomain(websocketDomain);
}
window.location.reload();
};
return (
<>
<Row style={{ alignItems: !isDetailed ? "center" : "flex-start" }}>
<Col xs={18}>
<Text>{t("pages.preferences.connectionDetailed")}</Text>
</Col>
<Col xs={6} style={{ textAlign: "right" }}>
<Switch
checkedChildren={<CheckOutlined />}
unCheckedChildren={<CloseOutlined />}
onChange={(checked: boolean) => {
setIsEnabled(checked);
if (!checked) {
setRpcDomain("");
setWebsocketDomain("");
setValue("rpcDomain", "");
setValue("websocketDomain", "");
}
}}
checked={isEnabled}
/>
</Col>
{isEnabled ? (
<Col xs={24}>
<form onSubmit={handleSubmit(onSubmit)}>
<div
style={{
display: "flex",
justifyContent: "flex-end",
marginTop: 12,
}}
>
<Space size={12} direction="vertical">
<Space size={3} direction="vertical">
<Text>{t("pages.preferences.rpcDomain")}</Text>
<Controller
render={({ field }) => (
<Input
{...field}
type="text"
style={{ width: "400px", maxWidth: "100%" }}
placeholder={`http://127.0.0.1:7076`}
maxLength={255}
suffix={
getValues("rpcDomain") && !errors?.rpcDomain ? (
<CheckCircleTwoTone twoToneColor={"#52c41a"} />
) : (
" "
)
}
/>
)}
rules={{
// @ts-ignore
validate: (value: string) => {
return !value || value.length >= 15;
},
}}
control={control}
name="rpcDomain"
defaultValue={getValues("rpcDomain")}
/>
</Space>
<Space size={3} direction="vertical">
<Text>{t("pages.preferences.websocketDomain")}</Text>
<Controller
render={({ field }) => (
<Input
{...field}
type="text"
style={{ width: "400px", maxWidth: "100%" }}
placeholder={`wss://www.nanolooker.com/ws`}
maxLength={255}
suffix={
getValues("websocketDomain") &&
!errors?.websocketDomain ? (
<CheckCircleTwoTone twoToneColor={"#52c41a"} />
) : (
" "
)
}
/>
)}
rules={{
// @ts-ignore
validate: (value: string) => {
return !value || value.length >= 15;
},
}}
control={control}
name="websocketDomain"
defaultValue={getValues("websocketDomain")}
/>
</Space>
</Space>
</div>
<div style={{ textAlign: "right", marginTop: 12 }}>
<Button
type="primary"
disabled={!isValid}
onClick={handleSubmit(onSubmit)}
>
Save
</Button>
</div>
</form>
</Col>
) : null}
</Row>
</>
);
}
Example #18
Source File: SortAction.tsx From datart with Apache License 2.0 | 4 votes |
SortAction: FC<{
config: ChartDataSectionField;
dataset?: ChartDataSetDTO;
onConfigChange: (
config: ChartDataSectionField,
needRefresh?: boolean,
) => void;
mode?: 'menu';
options?;
}> = ({ config, dataset, mode, options, onConfigChange }) => {
const actionNeedNewRequest = isEmpty(options?.backendSort)
? true
: Boolean(options?.backendSort);
const t = useI18NPrefix(`viz.palette.data.actions`);
const [direction, setDirection] = useState(
config?.sort?.type || SortActionType.NONE,
);
const [sortValue, setSortValue] = useState(() => {
const objDataColumns = transformToDataSet(dataset?.rows, dataset?.columns);
return (
config?.sort?.value ||
Array.from(new Set(objDataColumns?.map(c => c.getCell(config))))
);
});
const handleSortTypeChange = direction => {
setDirection(direction);
if (SortActionType.CUSTOMIZE !== direction) {
onConfigChange &&
onConfigChange(
updateBy(config, draft => {
draft.sort = { type: direction };
}),
actionNeedNewRequest,
);
}
};
const handleCustomSortListChange = values => {
setSortValue(values);
onConfigChange &&
onConfigChange(
updateBy(config, draft => {
draft.sort = { type: SortActionType.CUSTOMIZE, value: values };
}),
!actionNeedNewRequest,
);
};
const renderColumnsDataList = () => {
if (
!config.colName ||
SortActionType.CUSTOMIZE !== direction ||
!Array.isArray(sortValue)
) {
return null;
}
const items =
sortValue.map((value, index) => ({
id: index,
text: value,
})) || [];
return (
<DraggableList source={items} onChange={handleCustomSortListChange} />
);
};
const renderOptions = mode => {
if (mode === 'menu') {
return (
<>
{[SortActionType.NONE, SortActionType.ASC, SortActionType.DESC].map(
sort => {
return (
<Menu.Item
key={sort}
eventKey={sort}
icon={direction === sort ? <CheckOutlined /> : ''}
onClick={() => handleSortTypeChange(sort)}
>
{t(`sort.${sort?.toLowerCase()}`)}
</Menu.Item>
);
},
)}
</>
);
}
return (
<StyledRow>
<Col span={12}>
<Radio.Group
onChange={e => handleSortTypeChange(e.target?.value)}
value={direction}
>
<Space direction="vertical">
<Radio key={SortActionType.NONE} value={SortActionType.NONE}>
{t('sort.none')}
</Radio>
<Radio key={SortActionType.ASC} value={SortActionType.ASC}>
{t('sort.asc')}
</Radio>
<Radio key={SortActionType.DESC} value={SortActionType.DESC}>
{t('sort.desc')}
</Radio>
</Space>
</Radio.Group>
</Col>
{/* {SortActionType.CUSTOMIZE === direction && (
<Col span={12}>{renderColumnsDataList()}</Col>
)} */}
</StyledRow>
);
};
return renderOptions(mode);
}
Example #19
Source File: OrganizationList.tsx From datart with Apache License 2.0 | 4 votes |
export function OrganizationList() {
const [formVisible, setFormVisible] = useState(false);
const dispatch = useDispatch();
const history = useHistory();
const organizations = useSelector(selectOrganizations);
const orgId = useSelector(selectOrgId);
const listLoading = useSelector(selectOrganizationListLoading);
const t = useI18NPrefix('main.nav.organization');
const showForm = useCallback(() => {
setFormVisible(true);
}, []);
const hideForm = useCallback(() => {
setFormVisible(false);
}, []);
const menuSelect = useCallback(
({ key }) => {
if (key !== orgId) {
dispatch(switchOrganization(key));
history.push(`/organizations/${key}`);
}
},
[dispatch, history, orgId],
);
let list;
if (listLoading) {
list = (
<LoadingWrapper>
<LoadingOutlined />
</LoadingWrapper>
);
} else {
list = (
<StyledMenu
prefixCls="ant-dropdown-menu"
selectable={false}
onClick={menuSelect}
>
{organizations.map(o => {
const itemClass = classnames({
selected: orgId === o.id,
});
return (
<MenuListItem
key={o.id}
className={itemClass}
prefix={
<Avatar size="small" src={`${BASE_RESOURCE_URL}${o.avatar}`}>
{o.name.substr(0, 1).toUpperCase()}
</Avatar>
}
{...(orgId === o.id && {
suffix: <CheckOutlined className="icon" />,
})}
>
<p>{o.name}</p>
</MenuListItem>
);
})}
</StyledMenu>
);
}
return (
<Wrapper>
<Title>
<h2>{t('title')}</h2>
<ToolbarButton
size="small"
icon={<PlusOutlined />}
onClick={showForm}
/>
</Title>
{list}
<OrganizationForm visible={formVisible} onCancel={hideForm} />
</Wrapper>
);
}
Example #20
Source File: DefaultValue.tsx From datart with Apache License 2.0 | 4 votes |
DefaultValue = memo(
({ type, expression, disabled, value = [], onChange }: DefaultValueProps) => {
const [inputValue, setInputValue] = useState<any>(void 0);
const t = useI18NPrefix('variable');
useEffect(() => {
setInputValue(void 0);
}, [type]);
const saveRegular = useCallback(
(selectedValue?) => {
let validValue;
switch (type) {
case VariableValueTypes.String:
if (inputValue && (inputValue as string).trim()) {
validValue = inputValue;
}
break;
case VariableValueTypes.Number:
if (inputValue !== null && !Number.isNaN(inputValue)) {
validValue = inputValue;
}
break;
case VariableValueTypes.Date:
validValue = selectedValue;
break;
}
if (validValue !== void 0) {
onChange && onChange(value ? value.concat(validValue) : [validValue]);
setInputValue(void 0);
}
},
[value, type, inputValue, onChange],
);
const saveExpression = useCallback(
e => {
onChange && onChange([e.target.value]);
},
[onChange],
);
const inputChange = useCallback(e => {
setInputValue(e.target.value);
}, []);
const inputNumberChange = useCallback(val => {
setInputValue(val);
}, []);
const datePickerConfirm = useCallback(
val => {
saveRegular(val);
},
[saveRegular],
);
const tagClose = useCallback(
index => e => {
e.preventDefault();
onChange && onChange(value ? value.filter((_, i) => index !== i) : []);
},
[value, onChange],
);
let conditionalInputComponent;
switch (type) {
case VariableValueTypes.Number:
conditionalInputComponent = (
<InputNumber
placeholder={t('enterToAdd')}
value={inputValue}
className="input"
disabled={!!disabled}
onChange={inputNumberChange}
onPressEnter={saveRegular}
/>
);
break;
case VariableValueTypes.Date:
conditionalInputComponent = (
<DatePicker
format={TIME_FORMATTER}
className="input"
disabled={!!disabled}
onOk={datePickerConfirm}
showNow
showTime
/>
);
break;
default:
conditionalInputComponent = (
<Input
placeholder={t('enterToAdd')}
value={inputValue}
className="input"
disabled={!!disabled}
onChange={inputChange}
onPressEnter={saveRegular}
/>
);
break;
}
return (
<Wrapper direction="vertical" size={0}>
{expression || type === VariableValueTypes.Expression ? (
<Input.TextArea
placeholder={t('enterExpression')}
autoSize={{ minRows: 4, maxRows: 8 }}
value={value ? value[0] : void 0}
disabled={!!disabled}
onChange={saveExpression}
/>
) : (
<>
{value && value.length > 0 && (
<ValueTags key="valueTags">
{value?.map((val, index) => {
const label =
type !== VariableValueTypes.Date
? val
: moment(val).format(TIME_FORMATTER);
return (
<Tag
key={label}
className="tag"
closable
onClose={tagClose(index)}
>
{label}
</Tag>
);
})}
</ValueTags>
)}
<Space key="actions">
{conditionalInputComponent}
{type !== VariableValueTypes.Date && (
<Button
size="small"
icon={<CheckOutlined />}
type="link"
onClick={saveRegular}
/>
)}
</Space>
</>
)}
</Wrapper>
);
},
)
Example #21
Source File: ChartDrillContextMenu.tsx From datart with Apache License 2.0 | 4 votes |
ChartDrillContextMenu: FC<{ chartConfig?: ChartConfig }> = memo(
({ children, chartConfig }) => {
const t = useI18NPrefix(`viz.palette.drill`);
const {
drillOption,
onDrillOptionChange,
availableSourceFunctions,
onDateLevelChange,
} = useContext(ChartDrillContext);
const currentDrillLevel = drillOption?.getCurrentDrillLevel();
const runtimeDateLevelFields = useMemo(() => {
if (!drillOption) {
return;
}
const allFields = drillOption.getAllFields();
const currentFields = drillOption.getCurrentFields();
const groupSection = chartConfig?.datas?.find(
v => v.type === ChartDataSectionType.GROUP,
);
let rows: ChartDataSectionField[] | undefined = [];
if (currentFields) {
rows = groupSection?.rows?.filter(v =>
currentFields.some(val => val.uid === v.uid),
);
} else {
rows = groupSection?.rows?.filter(v => v.uid === allFields[0].uid);
}
return getRuntimeDateLevelFields(rows);
}, [drillOption, chartConfig?.datas]);
const handleDateLevelChange = useCallback(
(config: ChartDataSectionField) => {
const groupData = chartConfig?.datas?.find(
v => v.type === ChartDataSectionType.GROUP,
);
if (groupData) {
const _groupData = updateBy(groupData, draft => {
if (draft.rows) {
const index = draft.rows.findIndex(v => v.uid === config.uid);
const runtimeDateLevel =
draft.rows[index][RUNTIME_DATE_LEVEL_KEY];
const replacedColName = runtimeDateLevel
? runtimeDateLevel.colName
: draft.rows[index].colName;
draft.rows[index][RUNTIME_DATE_LEVEL_KEY] = config;
draft.replacedColName = replacedColName;
}
});
onDateLevelChange?.('data', {
needRefresh: true,
ancestors: [0],
value: _groupData,
});
}
},
[chartConfig?.datas, onDateLevelChange],
);
const selectDrillStatusMenu = useMemo(() => {
return (
<Menu.Item key="selectDrillStatus">
<StyledMenuSwitch
className={classnames({ on: !!drillOption?.isSelectedDrill })}
>
<p>
{drillOption?.isSelectedDrill
? t('selectDrillOn')
: t('selectDrillOff')}
</p>
<CheckOutlined className="icon" />
</StyledMenuSwitch>
</Menu.Item>
);
}, [drillOption?.isSelectedDrill, t]);
const contextMenu = useMemo(() => {
return (
<StyledChartDrillMenu
onClick={({ key }) => {
if (!drillOption) {
return;
}
if (key === 'selectDrillStatus') {
drillOption?.toggleSelectedDrill(!drillOption?.isSelectedDrill);
onDrillOptionChange?.(drillOption);
} else if (key === DrillMode.Drill) {
drillOption?.drillDown();
onDrillOptionChange?.(drillOption);
} else if (key === DrillMode.Expand) {
drillOption?.expandDown();
onDrillOptionChange?.(drillOption);
} else if (key === 'rollUp') {
drillOption?.rollUp();
onDrillOptionChange?.(drillOption);
}
}}
>
{!!currentDrillLevel && (
<Menu.Item key={'rollUp'}>{t('rollUp')}</Menu.Item>
)}
{drillOption?.mode !== DrillMode.Expand &&
!drillOption?.isBottomLevel && (
<Menu.Item key={DrillMode.Drill}>{t('showNextLevel')}</Menu.Item>
)}
{drillOption?.mode !== DrillMode.Drill &&
!drillOption?.isBottomLevel && (
<Menu.Item key={DrillMode.Expand}>
{t('expandNextLevel')}
</Menu.Item>
)}
{drillOption?.mode !== DrillMode.Expand && selectDrillStatusMenu}
{runtimeDateLevelFields?.map((v, i) => {
if (v.type === DataViewFieldType.DATE) {
return (
<Menu.SubMenu key={i} title={v.colName}>
<DateLevelMenuItems
availableSourceFunctions={availableSourceFunctions}
config={v[RUNTIME_DATE_LEVEL_KEY] || v}
onChange={config => handleDateLevelChange(config)}
/>
</Menu.SubMenu>
);
}
return false;
})}
</StyledChartDrillMenu>
);
}, [
currentDrillLevel,
t,
drillOption,
selectDrillStatusMenu,
runtimeDateLevelFields,
onDrillOptionChange,
handleDateLevelChange,
availableSourceFunctions,
]);
const hasContextMenu =
drillOption?.isDrillable || runtimeDateLevelFields?.length;
return (
<StyledChartDrill className="chart-drill-menu-container">
{hasContextMenu ? (
<Dropdown
disabled={!drillOption}
overlay={contextMenu}
destroyPopupOnHide={true}
trigger={['contextMenu']}
>
<div style={{ height: '100%' }}>{children}</div>
</Dropdown>
) : (
<div style={{ height: '100%' }}>{children}</div>
)}
</StyledChartDrill>
);
},
)
Example #22
Source File: PieChart.tsx From nanolooker with MIT License | 4 votes |
Representatives: React.FC<Props> = ({
isIncludeOfflineRepresentatives,
setIsIncludeOfflineRepresentatives,
isGroupedByEntities,
setIsGroupedByEntities,
}) => {
const { t } = useTranslation();
const { theme } = React.useContext(PreferencesContext);
const {
representatives,
isLoading: isRepresentativesLoading,
} = React.useContext(RepresentativesContext);
const [nakamotoCoefficient, setNakamotoCoefficient] = React.useState(
[] as Representative[],
);
const [
principalRepresentatives,
setPrincipalRepresentatives,
] = React.useState([] as Representative[]);
const {
confirmationQuorum: {
principal_representative_min_weight: principalRepresentativeMinWeight = 0,
online_weight_quorum_percent: onlineWeightQuorumPercent,
},
isLoading: isConfirmationQuorumLoading,
} = React.useContext(ConfirmationQuorumContext);
const [delegatedEntities, setDelegatedEntities] = React.useState(
[] as KnownAccountsBalance[],
);
const representativesSkeletonProps = {
active: true,
paragraph: true,
loading: isRepresentativesLoading,
};
React.useEffect(() => {
getDelegatedEntity().then(delegatedEntities => {
setDelegatedEntities(delegatedEntities || []);
});
}, []);
React.useEffect(() => {
if (
isRepresentativesLoading ||
isConfirmationQuorumLoading ||
!principalRepresentatives.length ||
!Array.isArray(delegatedEntities)
)
return;
const aliasSeparator = "|||";
// const stake = new BigNumber(rawToRai(onlineStakeTotal)).toNumber();
let filteredRepresentatives = isIncludeOfflineRepresentatives
? [...principalRepresentatives]
: [...principalRepresentatives].filter(({ isOnline }) => isOnline);
let stake = 0;
forEach(filteredRepresentatives, representative => {
stake = new BigNumber(stake).plus(representative.weight).toNumber();
});
if (isGroupedByEntities && delegatedEntities.length) {
// @TODO find a more scalable option
const groups: { [key: string]: number } = {
"Nano Foundation": 0,
Binance: 0,
Kraken: 0,
Huobi: 0,
Kucoin: 0,
};
// @ts-ignore added representative key
delegatedEntities.forEach(({ alias, account, representative, total }) => {
const accountIndex = filteredRepresentatives.findIndex(
({ account: representativeAccount }) =>
representativeAccount === account,
);
const representativeIndex = filteredRepresentatives.findIndex(
({ account }) => account === representative,
);
if (accountIndex > -1) {
filteredRepresentatives[accountIndex] = {
...filteredRepresentatives[accountIndex],
weight: filteredRepresentatives[accountIndex].weight + total,
};
} else {
filteredRepresentatives.push({
alias,
account,
isOnline: true,
isPrincipal: true,
weight: total,
});
}
if (representativeIndex > -1) {
filteredRepresentatives[representativeIndex] = {
...filteredRepresentatives[representativeIndex],
weight: filteredRepresentatives[representativeIndex].weight - total,
};
}
});
filteredRepresentatives = filteredRepresentatives.filter(
({ alias, weight }) => {
const group = alias
? Object.keys(groups).find(group =>
alias.toLowerCase()?.includes(group.toLowerCase()),
)
: null;
if (group) {
groups[group] = new BigNumber(groups[group])
.plus(weight)
.toNumber();
}
return !group;
},
);
const groupedEntities = Object.entries(groups).map(([group, weight]) => ({
account: "",
weight: weight,
isOnline: true,
isPrincipal: true,
alias: group,
}));
filteredRepresentatives = filteredRepresentatives
.concat(groupedEntities)
.filter(({ weight }) => weight >= principalRepresentativeMinWeight);
filteredRepresentatives = orderBy(
filteredRepresentatives,
["weight"],
["desc"],
);
}
const nakamotoCoefficient: Representative[] = [];
let nakamotoCoefficientWeight = 0;
let totalWeight = 0;
forEach(filteredRepresentatives, representative => {
const nextWeight = new BigNumber(nakamotoCoefficientWeight)
.plus(representative.weight)
.toNumber();
totalWeight = new BigNumber(totalWeight)
.plus(representative.weight)
.toNumber();
const percent = new BigNumber(nextWeight)
.times(100)
.dividedBy(stake)
.toNumber();
nakamotoCoefficientWeight = nextWeight;
nakamotoCoefficient.push(representative);
if (percent > parseInt(onlineWeightQuorumPercent)) {
return false;
}
});
setNakamotoCoefficient(nakamotoCoefficient);
const data = filteredRepresentatives.map(({ weight, account, alias }) => {
const value = parseFloat(
new BigNumber(weight).times(100).dividedBy(stake).toFixed(2),
);
return {
// @NOTE Remove symbol characters as they are causing the chart to crash
// on latest Safari & mobile chrome. Re-enable once it's fixed
alias: `${alias?.replace(/\W/g, " ") || ""}${aliasSeparator}${account}`,
value,
};
});
const config = {
data,
angleField: "value",
colorField: "alias",
radius: 0.8,
// theme: 'dark',
label: {
visible: true,
type: "outer",
style:
theme === Theme.DARK
? {
fill: "white",
stroke: "none",
}
: {
fill: "black",
stroke: "#fff",
},
},
legend: {
visible: true,
itemName: {
// @ts-ignore
formatter: (text: string) => {
const [alias, account] = text.split(aliasSeparator);
return alias || account || t("common.unknown");
},
},
},
tooltip: {
showTitle: false,
// @ts-ignore
formatter: ({ value, alias: rawAlias }) => {
const [alias, account] = rawAlias.split(aliasSeparator);
return {
name: alias || account || t("common.unknown"),
value: `${value}%`,
};
},
},
interactions: [{ type: "element-active" }],
};
if (!representativesChart) {
representativesChart = new Pie(
document.getElementById("representatives-chart") as HTMLElement,
// @ts-ignore
config,
);
representativesChart.render();
} else {
// @ts-ignore
representativesChart.update(config);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [
theme,
principalRepresentatives,
isRepresentativesLoading,
isConfirmationQuorumLoading,
isIncludeOfflineRepresentatives,
isGroupedByEntities,
delegatedEntities,
]);
React.useEffect(() => {
if (isRepresentativesLoading || !representatives.length) return;
const filteredRepresentatives = representatives.filter(
({ isPrincipal }) => isPrincipal,
);
setPrincipalRepresentatives(filteredRepresentatives);
}, [representatives, isRepresentativesLoading]);
React.useEffect(() => {
return () => {
representativesChart?.destroy();
representativesChart = null;
};
}, []);
return (
<>
<Title level={3}>{t("pages.representatives.voteDistribution")}</Title>
<Card size="small" bordered={false} className="detail-layout">
<Row gutter={6}>
<Col xs={20} md={12}>
{t("pages.representatives.includeOfflineRepresentatives")}
</Col>
<Col xs={4} md={12}>
<Switch
disabled={isRepresentativesLoading}
checkedChildren={<CheckOutlined />}
unCheckedChildren={<CloseOutlined />}
onChange={(checked: boolean) => {
setIsIncludeOfflineRepresentatives(checked);
}}
defaultChecked={isIncludeOfflineRepresentatives}
/>
</Col>
</Row>
<Row gutter={6}>
<Col xs={20} md={12}>
{t("pages.representatives.groupByEntities")}
<Tooltip placement="right" title={t("tooltips.groupByEntities")}>
<QuestionCircle />
</Tooltip>
</Col>
<Col xs={4} md={12}>
<Switch
disabled={isRepresentativesLoading || !delegatedEntities.length}
checkedChildren={<CheckOutlined />}
unCheckedChildren={<CloseOutlined />}
onChange={(checked: boolean) => {
setIsGroupedByEntities(checked);
}}
checked={
// Ensure the API returns delegatedEntities to enable the switch
delegatedEntities.length ? isGroupedByEntities : false
}
/>
</Col>
</Row>
<Row>
<Col xs={24} md={12}>
{t("pages.representatives.nakamotoCoefficient")}
{onlineWeightQuorumPercent ? (
<Tooltip
placement="right"
title={t("tooltips.nakamotoCoefficient", {
onlineWeightQuorumPercent: onlineWeightQuorumPercent,
})}
>
<QuestionCircle />
</Tooltip>
) : null}
</Col>
<Col xs={24} md={12}>
<Skeleton
active
paragraph={false}
loading={!nakamotoCoefficient.length}
>
<Text>{nakamotoCoefficient.length}</Text>
</Skeleton>
</Col>
</Row>
<Row>
<Col xs={24}>
<Text style={{ fontSize: "12px" }}>
{t("pages.representatives.voteDistributionDescription")}
</Text>
</Col>
</Row>
<Skeleton {...representativesSkeletonProps}>
<div id="representatives-chart" />
</Skeleton>
</Card>
</>
);
}
Example #23
Source File: PieChart.tsx From nanolooker with MIT License | 4 votes |
Representatives: React.FC<Props> = ({ versions }) => {
const { t } = useTranslation();
const { theme } = React.useContext(PreferencesContext);
const [isVersionByWeight, setIsVersionByWeight] = React.useState(true);
const {
confirmationQuorum: {
online_weight_quorum_percent: onlineWeightQuorumPercent = 0,
online_weight_minimum: onlineWeightMinimum = 0,
},
} = React.useContext(ConfirmationQuorumContext);
React.useEffect(() => {
if (!Object.keys(versions).length) return;
let data = orderBy(
Object.entries(versions).map(([version, { weight, count }]) => ({
version,
weight,
count,
})),
["version"],
["desc"],
);
let totalWeight = 0;
if (isVersionByWeight) {
data = data.filter(({ weight }) => {
totalWeight += weight;
return !!weight;
});
}
const config = {
padding: -12,
data,
angleField: isVersionByWeight ? "weight" : "count",
colorField: "version",
radius: 0.8,
label: {
visible: true,
type: "outer",
// @ts-ignore
formatter: (text, item, index) => {
return `${item._origin.version}`;
},
style:
theme === Theme.DARK
? {
fill: "white",
stroke: "none",
}
: {
fill: "black",
stroke: "#fff",
},
},
legend: {
visible: false,
},
tooltip: {
showTitle: false,
formatter: ({
weight,
count,
version,
}: {
weight: number;
count: number;
version: string;
}) => ({
name: version,
value: isVersionByWeight
? `Ӿ ${new BigNumber(weight).toFormat(2)} - ${new BigNumber(weight)
.times(100)
.dividedBy(totalWeight)
.toFormat(2)}%`
: `${count} ${t("common.nodes")}`,
}),
},
interactions: [{ type: "element-active" }],
};
if (!versionsChart) {
versionsChart = new Pie(
document.getElementById("versions-chart") as HTMLElement,
// @ts-ignore
config,
);
versionsChart.render();
} else {
versionsChart.update(config);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [theme, versions, isVersionByWeight]);
React.useEffect(() => {
return () => {
versionsChart?.destroy();
versionsChart = null;
};
}, []);
return (
<>
<Title level={3}>{t("pages.status.nodeVersions")}</Title>
<Card size="small" bordered={false} className="detail-layout">
<Row gutter={6}>
<Col xs={20} md={12}>
{t("pages.status.versionsByWeight")}
<Tooltip
placement="right"
title={t("tooltips.versionsByWeight", {
onlineWeightMinimum: new BigNumber(
rawToRai(onlineWeightMinimum),
).toFormat(),
onlineWeightQuorumPercent,
})}
>
<QuestionCircle />
</Tooltip>
</Col>
<Col xs={4} md={12}>
<Switch
disabled={!Object.keys(versions).length}
checkedChildren={<CheckOutlined />}
unCheckedChildren={<CloseOutlined />}
onChange={(checked: boolean) => {
setIsVersionByWeight(checked);
}}
defaultChecked={isVersionByWeight}
/>
</Col>
</Row>
<Row>
<Col xs={24}>
<Skeleton loading={!Object.keys(versions).length} active>
<div id="versions-chart" />
</Skeleton>
</Col>
</Row>
</Card>
</>
);
}
Example #24
Source File: index.tsx From nanolooker with MIT License | 4 votes |
Distribution: React.FC = () => {
const { t } = useTranslation();
const {
knownExchangeAccounts,
isLoading: isKnownAccountsLoading,
} = React.useContext(KnownAccountsContext);
const [isIncludeExchanges, setIsIncludeExchanges] = React.useState<boolean>(
true,
);
const [totalAccounts, setTotalAccounts] = React.useState<number>(0);
const [totalBalance, setTotalBalance] = React.useState<number>(0);
const [distributionData, setDistributionData] = React.useState<any[]>([]);
const [isLogScale, setIsLogScale] = React.useState<boolean>(false);
const [knownExchangeBalance, setKnownExchangeBalance] = React.useState("0");
const { data } = useDistribution();
React.useEffect(() => {
return () => {
distributionChart?.destroy();
distributionChart = null;
};
}, []);
React.useEffect(() => {
if (
!data?.distribution ||
!data?.knownExchanges ||
!knownExchangeAccounts.length
)
return;
let knownExchangeDistribution: DistributionIndex[] = [];
if (!isIncludeExchanges) {
Object.values(data.knownExchanges).forEach(balance => {
let index = balance >= 1 ? `${Math.floor(balance)}`.length : 0;
knownExchangeDistribution[index] = {
accounts: (knownExchangeDistribution[index]?.accounts || 0) + 1,
balance: new BigNumber(balance)
.plus(knownExchangeDistribution[index]?.balance || 0)
.toNumber(),
};
});
}
const tmpDistributionData: any[] = [];
let tmpTotalAccounts = 0;
let tmpTotalBalance = 0;
data.distribution.forEach(
(
{ accounts, balance }: { accounts: number; balance: number },
i: number,
): void => {
const calcAccounts =
accounts - (knownExchangeDistribution[i]?.accounts || 0);
let calcBalance = new BigNumber(balance)
.minus(knownExchangeDistribution[i]?.balance || 0)
.toNumber();
if (calcBalance < 0 && !calcAccounts) {
calcBalance = 0;
}
tmpTotalAccounts += calcAccounts;
tmpTotalBalance += calcBalance;
tmpDistributionData.push({
title: distributionMap[i],
value: calcBalance,
type: "balance",
});
tmpDistributionData.push({
title: distributionMap[i],
value: calcAccounts,
type: "accounts",
});
},
);
const knownExchangeBalance = new BigNumber(
Object.values(data?.knownExchanges || []).reduce(
(acc, balance) => new BigNumber(acc).plus(balance).toNumber(),
0,
),
).toFormat(2);
setKnownExchangeBalance(knownExchangeBalance);
setTotalAccounts(tmpTotalAccounts);
setTotalBalance(tmpTotalBalance);
setDistributionData(tmpDistributionData);
}, [data, isIncludeExchanges, knownExchangeAccounts]);
useDeepCompareEffect(() => {
// @TODO: Validate data: https://nanocharts.info/p/05/balance-distribution
// https://g2plot.antv.vision/en/examples/column/stacked#connected-area-interaction
const config = {
forceFit: true,
responsive: true,
padding: "auto",
isStack: true,
data: distributionData,
xField: "title",
yField: "value",
seriesField: "type",
yAxis: {
type: isLogScale ? "log" : "linear",
min: 0,
base: 2,
},
meta: {
value: {
alias: " ",
formatter: (text: number) => {
return intToString(text);
},
},
title: {
alias: " ",
},
},
tooltip: {
// @ts-ignore
formatter: ({ title, value, name }) => ({
title,
value: new BigNumber(value).toFormat(),
name: isInteger(value) ? t("common.accounts") : t("common.balance"),
}),
},
legend: {
layout: "horizontal",
position: "top",
itemName: {
style: {
fontSize: 14,
},
formatter: (text: string) => t(`common.${text}`),
},
},
};
if (!distributionChart) {
distributionChart = new Column(
document.getElementById("distribution-chart") as HTMLElement,
// @ts-ignore
config,
);
distributionChart.render();
} else {
distributionChart.update(config);
}
}, [distributionData, isLogScale]);
const i18nTotalAccounts = new BigNumber(totalAccounts).toFormat();
const i18nTotalBalances = new BigNumber(totalBalance).toFormat();
const knownExchangeList = knownExchangeAccounts
.map(({ alias }) => alias)
.join(", ");
const date = data?.status?.date || t("common.notAvailable");
return (
<>
<Helmet>
<title>Nano {t("menu.distribution")}</title>
</Helmet>
<Title level={3}>{t("pages.distribution.title")}</Title>
<Card size="small">
<div style={{ marginBottom: "12px" }}>
<Text style={{ fontSize: "12px" }}>
{t("common.executionTimeAgo")}{" "}
<TimeAgo
locale={i18next.language}
datetime={date}
live={false}
style={{ fontWeight: "bold" }}
/>
</Text>
<br />
<Text style={{ fontSize: "12px" }}>
<Trans i18nKey="pages.distribution.summary">
<strong>{{ i18nTotalAccounts }}</strong>
<strong>{{ i18nTotalBalances }}</strong>
</Trans>
</Text>
<br />
<Text style={{ fontSize: "12px" }}>
<Trans i18nKey="pages.distribution.summaryMinBalance">
<strong />
</Trans>
</Text>
</div>
<div style={{ marginBottom: "6px" }}>
<Switch
disabled={isKnownAccountsLoading}
checkedChildren={<CheckOutlined />}
unCheckedChildren={<CloseOutlined />}
onChange={(checked: boolean) => {
setIsIncludeExchanges(checked);
}}
defaultChecked={isIncludeExchanges}
/>
<Text style={{ marginLeft: "6px" }}>
{t("pages.distribution.includeKnownExchanges")}
</Text>
<Tooltip
placement="right"
title={t("tooltips.knownExchangeBalance", {
knownExchangeList,
knownExchangeBalance,
})}
>
<QuestionCircle />
</Tooltip>
</div>
<div style={{ marginBottom: "6px" }}>
<Switch
disabled={isKnownAccountsLoading}
checkedChildren={<CheckOutlined />}
unCheckedChildren={<CloseOutlined />}
onChange={(checked: boolean) => {
setIsLogScale(checked);
}}
defaultChecked={isLogScale}
/>
<Text style={{ margin: "0 6px" }}>
{t("pages.distribution.logScale")}
</Text>
</div>
<div style={{ marginTop: 24 }} id="distribution-chart" />
</Card>
<RichList />
<DormantFunds data={data?.dormantFunds} />
</>
);
}
Example #25
Source File: TAQueueDetailButtons.tsx From office-hours with GNU General Public License v3.0 | 4 votes |
export default function TAQueueDetailButtons({
courseId,
queueId,
question,
hasUnresolvedRephraseAlert,
}: {
courseId: number;
queueId: number;
question: Question;
hasUnresolvedRephraseAlert: boolean;
}): ReactElement {
const defaultMessage = useDefaultMessage();
const { mutateQuestions } = useQuestions(queueId);
const changeStatus = useCallback(
async (status: QuestionStatus) => {
await API.questions.update(question.id, { status });
mutateQuestions();
},
[question.id, mutateQuestions]
);
const { isCheckedIn, isHelping } = useTAInQueueInfo(queueId);
const openTeams = useTeams(queueId, question.creator.email, defaultMessage);
// eslint-disable-next-line @typescript-eslint/no-unused-vars
const sendRephraseAlert = async () => {
const payload: RephraseQuestionPayload = {
queueId,
questionId: question.id,
courseId,
};
try {
await API.alerts.create({
alertType: AlertType.REPHRASE_QUESTION,
courseId,
payload,
targetUserId: question.creator.id,
});
await mutateQuestions();
message.success("Successfully asked student to rephrase their question.");
} catch (e) {
//If the ta creates an alert that already exists the error is caught and nothing happens
}
};
const helpStudent = () => {
changeStatus(OpenQuestionStatus.Helping);
openTeams();
};
const deleteQuestion = async () => {
await changeStatus(
question.status === OpenQuestionStatus.Drafting
? ClosedQuestionStatus.DeletedDraft
: LimboQuestionStatus.TADeleted
);
await API.questions.notify(question.id);
};
useHotkeys(
"shift+d",
() => {
if (isCheckedIn) {
deleteQuestion();
}
},
[question]
);
if (question.status === OpenQuestionStatus.Helping) {
return (
<>
<Popconfirm
title="Are you sure you want to send this student back to the queue?"
okText="Yes"
cancelText="No"
onConfirm={async () => {
message.success(PRORITY_QUEUED_MESSAGE_TEXT, 2);
await changeStatus(LimboQuestionStatus.ReQueueing);
}}
>
<Tooltip title="Requeue Student">
<RequeueButton
icon={<UndoOutlined />}
data-cy="requeue-student-button"
/>
</Tooltip>
</Popconfirm>
<Popconfirm
title="Are you sure you can't find this student?"
okText="Yes"
cancelText="No"
onConfirm={async () => {
message.success(PRORITY_QUEUED_MESSAGE_TEXT, 2);
await changeStatus(LimboQuestionStatus.CantFind);
await API.questions.notify(question.id);
}}
>
<Tooltip title="Can't Find">
<CantFindButton
shape="circle"
icon={<CloseOutlined />}
data-cy="cant-find-button"
/>
</Tooltip>
</Popconfirm>
<Tooltip title="Finish Helping">
<FinishHelpingButton
icon={<CheckOutlined />}
onClick={() => changeStatus(ClosedQuestionStatus.Resolved)}
data-cy="finish-helping-button"
/>
</Tooltip>
</>
);
} else {
const [canHelp, helpTooltip] = ((): [boolean, string] => {
if (!isCheckedIn) {
return [false, "You must check in to help students!"];
} else if (isHelping) {
return [false, "You are already helping a student"];
} else {
return [true, "Help Student"];
}
})();
const [canRephrase, rephraseTooltip] = ((): [boolean, string] => {
if (!isCheckedIn) {
return [
false,
"You must check in to ask this student to rephrase their question",
];
} else if (hasUnresolvedRephraseAlert) {
return [
false,
"The student has already been asked to rephrase their question",
];
} else if (question.status === OpenQuestionStatus.Drafting) {
return [
false,
"The student must finish drafting before they can be asked to rephrase their question",
];
} else {
return [true, "Ask the student to add more detail to their question"];
}
})();
return (
<>
<Popconfirm
title="Are you sure you want to delete this question from the queue?"
disabled={!isCheckedIn}
okText="Yes"
cancelText="No"
onConfirm={async () => {
await deleteQuestion();
}}
>
<Tooltip
title={
isCheckedIn
? "Remove From Queue"
: "You must check in to remove students from the queue"
}
>
<span>
{/* This span is a workaround for tooltip-on-disabled-button
https://github.com/ant-design/ant-design/issues/9581#issuecomment-599668648 */}
<BannerDangerButton
shape="circle"
icon={<DeleteOutlined />}
data-cy="remove-from-queue"
disabled={!isCheckedIn}
/>
</span>
</Tooltip>
</Popconfirm>
<Tooltip title={rephraseTooltip}>
<span>
<BannerOrangeButton
shape="circle"
icon={<QuestionOutlined />}
onClick={sendRephraseAlert}
data-cy="request-rephrase-question"
disabled={!canRephrase}
/>
</span>
</Tooltip>
<Tooltip title={helpTooltip}>
<span>
<BannerPrimaryButton
icon={<PhoneOutlined />}
onClick={() => helpStudent()}
disabled={!canHelp}
data-cy="help-student"
/>
</span>
</Tooltip>
</>
);
}
}
Example #26
Source File: HTTPFlowTable.tsx From yakit with GNU Affero General Public License v3.0 | 4 votes |
HTTPFlowTable: React.FC<HTTPFlowTableProp> = (props) => {
const [data, setData, getData] = useGetState<HTTPFlow[]>([])
const [params, setParams] = useState<YakQueryHTTPFlowRequest>(
props.params || {SourceType: "mitm"}
)
const [pagination, setPagination] = useState<PaginationSchema>({
Limit: OFFSET_LIMIT,
Order: "desc",
OrderBy: "created_at",
Page: 1
});
// const [autoReload, setAutoReload, getAutoReload] = useGetState(false);
const autoReloadRef = useRef<boolean>(false);
const autoReload = autoReloadRef.current;
const setAutoReload = (b: boolean) => {
autoReloadRef.current = b
};
const getAutoReload = () => autoReloadRef.current;
const [total, setTotal] = useState<number>(0)
const [loading, setLoading] = useState(false)
const [selected, setSelected, getSelected] = useGetState<HTTPFlow>()
const [_lastSelected, setLastSelected, getLastSelected] = useGetState<HTTPFlow>()
const [compareLeft, setCompareLeft] = useState<CompateData>({content: '', language: 'http'})
const [compareRight, setCompareRight] = useState<CompateData>({content: '', language: 'http'})
const [compareState, setCompareState] = useState(0)
const [tableContentHeight, setTableContentHeight, getTableContentHeight] = useGetState<number>(0);
// 用于记录适合
const [_scrollY, setScrollYRaw, getScrollY] = useGetState(0)
const setScrollY = useThrottleFn(setScrollYRaw, {wait: 300}).run
// 如果这个大于等于 0 ,就 Lock 住,否则忽略
const [_trigger, setLockedScroll, getLockedScroll] = useGetState(-1);
const lockScrollTimeout = (size: number, timeout: number) => {
setLockedScroll(size)
setTimeout(() => setLockedScroll(-1), timeout)
}
const tableRef = useRef(null)
const ref = useHotkeys('ctrl+r, enter', e => {
const selected = getSelected()
if (selected) {
ipcRenderer.invoke("send-to-tab", {
type: "fuzzer",
data: {
isHttps: selected?.IsHTTPS,
request: new Buffer(selected.Request).toString()
}
})
}
})
// 使用上下箭头
useHotkeys("up", () => {
setLastSelected(getSelected())
const data = getData();
if (data.length <= 0) {
return
}
if (!getSelected()) {
setSelected(data[0])
return
}
const expected = parseInt(`${parseInt(`${(getSelected()?.Id as number)}`) + 1}`);
// 如果上点的话,应该是选择更新的内容
for (let i = 0; i < data.length; i++) {
let current = parseInt(`${data[i]?.Id}`);
if (current === expected) {
setSelected(data[i])
return
}
}
setSelected(undefined)
})
useHotkeys("down", () => {
setLastSelected(getSelected())
const data = getData();
if (data.length <= 0) {
return
}
if (!getSelected()) {
setSelected(data[0])
return
}
// 如果上点的话,应该是选择更新的内容
for (let i = 0; i < data.length; i++) {
if (data[i]?.Id == (getSelected()?.Id as number) - 1) {
setSelected(data[i])
return
}
}
setSelected(undefined)
})
// 向主页发送对比数据
useEffect(() => {
if (compareLeft.content) {
const params = {info: compareLeft, type: 1}
setCompareState(compareState === 0 ? 1 : 0)
ipcRenderer.invoke("add-data-compare", params)
}
}, [compareLeft])
useEffect(() => {
if (compareRight.content) {
const params = {info: compareRight, type: 2}
setCompareState(compareState === 0 ? 2 : 0)
ipcRenderer.invoke("add-data-compare", params)
}
}, [compareRight])
const update = useMemoizedFn((
page?: number,
limit?: number,
order?: string,
orderBy?: string,
sourceType?: string,
noLoading?: boolean
) => {
const paginationProps = {
Page: page || 1,
Limit: limit || pagination.Limit,
Order: order || "desc",
OrderBy: orderBy || "id"
}
if (!noLoading) {
setLoading(true)
// setAutoReload(false)
}
// yakQueryHTTPFlow({
// SourceType: sourceType, ...params,
// Pagination: {...paginationProps},
// })
ipcRenderer
.invoke("QueryHTTPFlows", {
SourceType: sourceType,
...params,
Pagination: {...paginationProps}
})
.then((rsp: YakQueryHTTPFlowResponse) => {
setData((rsp?.Data || []))
setPagination(rsp.Pagination)
setTotal(rsp.Total)
})
.catch((e: any) => {
failed(`query HTTP Flow failed: ${e}`)
})
.finally(() => setTimeout(() => setLoading(false), 300))
})
const getNewestId = useMemoizedFn(() => {
let max = 0;
(getData() || []).forEach(e => {
const id = parseInt(`${e.Id}`)
if (id >= max) {
max = id
}
})
return max
})
const getOldestId = useMemoizedFn(() => {
if (getData().length <= 0) {
return 0
}
let min = parseInt(`${getData()[0].Id}`);
(getData() || []).forEach(e => {
const id = parseInt(`${e.Id}`)
if (id <= min) {
min = id
}
})
return min
})
// 第一次启动的时候加载一下
useEffect(() => {
update(1)
}, [])
const scrollTableTo = useMemoizedFn((size: number) => {
if (!tableRef || !tableRef.current) return
const table = tableRef.current as unknown as {
scrollTop: (number) => any,
scrollLeft: (number) => any,
}
table.scrollTop(size)
})
const scrollUpdateTop = useDebounceFn(useMemoizedFn(() => {
const paginationProps = {
Page: 1,
Limit: OFFSET_STEP,
Order: "desc",
OrderBy: "id"
}
const offsetId = getNewestId()
console.info("触顶:", offsetId)
// 查询数据
ipcRenderer
.invoke("QueryHTTPFlows", {
SourceType: "mitm",
...params,
AfterId: offsetId, // 用于计算增量的
Pagination: {...paginationProps}
})
.then((rsp: YakQueryHTTPFlowResponse) => {
const offsetDeltaData = (rsp?.Data || [])
if (offsetDeltaData.length <= 0) {
// 没有增量数据
return
}
setLoading(true)
let offsetData = offsetDeltaData.concat(data);
if (offsetData.length > MAX_ROW_COUNT) {
offsetData = offsetData.splice(0, MAX_ROW_COUNT)
}
setData(offsetData);
scrollTableTo((offsetDeltaData.length + 1) * ROW_HEIGHT)
})
.catch((e: any) => {
failed(`query HTTP Flow failed: ${e}`)
})
.finally(() => setTimeout(() => setLoading(false), 200))
}), {wait: 600, leading: true, trailing: false}).run
const scrollUpdateButt = useDebounceFn(useMemoizedFn((tableClientHeight: number) => {
const paginationProps = {
Page: 1,
Limit: OFFSET_STEP,
Order: "desc",
OrderBy: "id"
}
const offsetId = getOldestId();
console.info("触底:", offsetId)
// 查询数据
ipcRenderer
.invoke("QueryHTTPFlows", {
SourceType: "mitm",
...params,
BeforeId: offsetId, // 用于计算增量的
Pagination: {...paginationProps}
})
.then((rsp: YakQueryHTTPFlowResponse) => {
const offsetDeltaData = (rsp?.Data || [])
if (offsetDeltaData.length <= 0) {
// 没有增量数据
return
}
setLoading(true)
const originDataLength = data.length;
let offsetData = data.concat(offsetDeltaData);
let metMax = false
const originOffsetLength = offsetData.length;
if (originOffsetLength > MAX_ROW_COUNT) {
metMax = true
offsetData = offsetData.splice(originOffsetLength - MAX_ROW_COUNT, MAX_ROW_COUNT)
}
setData(offsetData);
setTimeout(() => {
if (!metMax) {
// 没有丢结果的裁剪问题
scrollTableTo((originDataLength + 1) * ROW_HEIGHT - tableClientHeight)
} else {
// 丢了结果之后的裁剪计算
const a = originOffsetLength - offsetDeltaData.length;
scrollTableTo((originDataLength + 1 + MAX_ROW_COUNT - originOffsetLength) * ROW_HEIGHT - tableClientHeight)
}
}, 50)
})
.catch((e: any) => {
failed(`query HTTP Flow failed: ${e}`)
}).finally(() => setTimeout(() => setLoading(false), 60))
}), {wait: 600, leading: true, trailing: false}).run
const sortFilter = useMemoizedFn((column: string, type: any) => {
const keyRelation: any = {
UpdatedAt: "updated_at",
BodyLength: "body_length",
StatusCode: "status_code"
}
if (column && type) {
update(1, OFFSET_LIMIT, type, keyRelation[column])
} else {
update(1, OFFSET_LIMIT)
}
})
// 这是用来设置选中坐标的,不需要做防抖
useEffect(() => {
if (!getLastSelected() || !getSelected()) {
return
}
const lastSelected = getLastSelected() as HTTPFlow;
const up = parseInt(`${lastSelected?.Id}`) < parseInt(`${selected?.Id}`)
// if (up) {
// console.info("up")
// } else {
// console.info("down")
// }
// console.info(lastSelected.Id, selected?.Id)
const screenRowCount = Math.floor(getTableContentHeight() / ROW_HEIGHT) - 1
if (!autoReload) {
let count = 0;
const data = getData();
for (let i = 0; i < data.length; i++) {
if (data[i].Id != getSelected()?.Id) {
count++
} else {
break
}
}
let minCount = count
if (minCount < 0) {
minCount = 0
}
const viewHeightMin = getScrollY() + tableContentHeight
const viewHeightMax = getScrollY() + tableContentHeight * 2
const minHeight = minCount * ROW_HEIGHT;
const maxHeight = minHeight + tableContentHeight
const maxHeightBottom = minHeight + tableContentHeight + 3 * ROW_HEIGHT
// console.info("top: ", minHeight, "maxHeight: ", maxHeight, "maxHeightBottom: ", maxHeightBottom)
// console.info("viewTop: ", viewHeightMin, "viewButtom: ", viewHeightMax)
if (maxHeight < viewHeightMin) {
// 往下滚动
scrollTableTo(minHeight)
return
}
if (maxHeightBottom > viewHeightMax) {
// 上滚动
const offset = minHeight - (screenRowCount - 2) * ROW_HEIGHT;
// console.info(screenRowCount, minHeight, minHeight - (screenRowCount - 1) * ROW_HEIGHT)
if (offset > 0) {
scrollTableTo(offset)
}
return
}
}
}, [selected])
// 给设置做防抖
useDebounceEffect(() => {
props.onSelected && props.onSelected(selected)
}, [selected], {wait: 400, trailing: true, leading: true})
useEffect(() => {
if (autoReload) {
const id = setInterval(() => {
update(1, undefined, "desc", undefined, undefined, true)
}, 1000)
return () => {
clearInterval(id)
}
}
}, [autoReload])
return (
// <AutoCard bodyStyle={{padding: 0, margin: 0}} bordered={false}>
<div ref={ref as Ref<any>} tabIndex={-1}
style={{width: "100%", height: "100%", overflow: "hidden"}}
>
<ReactResizeDetector
onResize={(width, height) => {
if (!width || !height) {
return
}
setTableContentHeight(height - 38)
}}
handleWidth={true} handleHeight={true} refreshMode={"debounce"} refreshRate={50}/>
{!props.noHeader && (
<PageHeader
title={"HTTP History"}
subTitle={
<Space>
{"所有相关请求都在这里"}
<Button
icon={<ReloadOutlined/>}
type={"link"}
onClick={(e) => {
update(1)
}}
/>
</Space>
}
extra={[
<Space>
<Form.Item label={"选择 HTTP History 类型"} style={{marginBottom: 0}}>
<Select
mode={"multiple"}
value={params.SourceType}
style={{minWidth: 200}}
onChange={(e) => {
setParams({...params, SourceType: e})
setLoading(true)
setTimeout(() => {
update(1, undefined, undefined, undefined, e)
}, 200)
}}
>
<Select.Option value={"mitm"}>mitm: 中间人劫持</Select.Option>
<Select.Option value={"fuzzer"}>
fuzzer: 模糊测试分析
</Select.Option>
</Select>
</Form.Item>
<Popconfirm
title={"确定想要删除所有记录吗?不可恢复"}
onConfirm={(e) => {
ipcRenderer.invoke("delete-http-flows-all")
setLoading(true)
info("正在删除...如自动刷新失败请手动刷新")
setTimeout(() => {
update(1)
if (props.onSelected) props.onSelected(undefined)
}, 400)
}}
>
<Button danger={true}>清除全部历史记录?</Button>
</Popconfirm>
</Space>
]}
/>
)}
<Row style={{margin: "5px 0 5px 5px"}}>
<Col span={12}>
<Space>
<span>HTTP History</span>
<Button
icon={<ReloadOutlined/>}
type={"link"}
size={"small"}
onClick={(e) => {
update(1, undefined, "desc")
}}
/>
{/* <Space>
自动刷新:
<Switch size={"small"} checked={autoReload} onChange={setAutoReload}/>
</Space> */}
<Input.Search
placeholder={"URL关键字"}
enterButton={true}
size={"small"}
style={{width: 170}}
value={params.SearchURL}
onChange={(e) => {
setParams({...params, SearchURL: e.target.value})
}}
onSearch={(v) => {
update(1)
}}
/>
{props.noHeader && (
<Popconfirm
title={"确定想要删除所有记录吗?不可恢复"}
onConfirm={(e) => {
ipcRenderer.invoke("delete-http-flows-all")
setLoading(true)
info("正在删除...如自动刷新失败请手动刷新")
setCompareLeft({content: '', language: 'http'})
setCompareRight({content: '', language: 'http'})
setCompareState(0)
setTimeout(() => {
update(1)
if (props.onSelected) props.onSelected(undefined)
}, 400)
}}
>
<Button danger={true} size={"small"}>
删除历史记录
</Button>
</Popconfirm>
)}
{/*{autoReload && <Tag color={"green"}>自动刷新中...</Tag>}*/}
</Space>
</Col>
<Col span={12} style={{textAlign: "right"}}>
<Tag>{total} Records</Tag>
</Col>
</Row>
<TableResizableColumn
tableRef={tableRef}
virtualized={true}
className={"httpFlowTable"}
loading={loading}
columns={[
{
dataKey: "Id",
width: 80,
headRender: () => "序号",
cellRender: ({rowData, dataKey, ...props}: any) => {
return `${rowData[dataKey] <= 0 ? "..." : rowData[dataKey]}`
}
},
{
dataKey: "Method",
width: 70,
headRender: (params1: any) => {
return (
<div
style={{display: "flex", justifyContent: "space-between"}}
>
方法
<Popover
placement='bottom'
trigger='click'
content={
params &&
setParams && (
<HTTLFlowFilterDropdownForms
label={"搜索方法"}
params={params}
setParams={setParams}
filterName={"Methods"}
autoCompletions={["GET", "POST", "HEAD"]}
submitFilter={() => update(1)}
/>
)
}
>
<Button
style={{
paddingLeft: 4, paddingRight: 4, marginLeft: 4,
color: !!params.Methods ? undefined : "gray",
}}
type={!!params.Methods ? "primary" : "link"} size={"small"}
icon={<SearchOutlined/>}
/>
</Popover>
</div>
)
},
cellRender: ({rowData, dataKey, ...props}: any) => {
// return (
// <Tag color={"geekblue"} style={{marginRight: 20}}>
// {rowData[dataKey]}
// </Tag>
// )
return rowData[dataKey]
}
},
{
dataKey: "StatusCode",
width: 100,
sortable: true,
headRender: () => {
return (
<div
style={{display: "inline-flex"}}
>
状态码
<Popover
placement='bottom'
trigger='click'
content={
params &&
setParams && (
<HTTLFlowFilterDropdownForms
label={"搜索状态码"}
params={params}
setParams={setParams}
filterName={"StatusCode"}
autoCompletions={[
"200",
"300-305",
"400-404",
"500-502",
"200-299",
"300-399",
"400-499"
]}
submitFilter={() => update(1)}
/>
)
}
>
<Button
style={{
paddingLeft: 4, paddingRight: 4, marginLeft: 4,
color: !!params.StatusCode ? undefined : "gray",
}}
type={!!params.StatusCode ? "primary" : "link"} size={"small"}
icon={<SearchOutlined/>}
/>
</Popover>
</div>
)
},
cellRender: ({rowData, dataKey, ...props}: any) => {
return (
<div style={{color: StatusCodeToColor(rowData[dataKey])}}>
{rowData[dataKey] === 0 ? "" : rowData[dataKey]}
</div>
)
}
},
{
dataKey: "Url",
resizable: true,
headRender: () => {
return (
<div
style={{display: "flex", justifyContent: "space-between"}}
>
URL
<Popover
placement='bottom'
trigger='click'
content={
params &&
setParams && (
<HTTLFlowFilterDropdownForms
label={"搜索URL关键字"}
params={params}
setParams={setParams}
filterName={"SearchURL"}
pureString={true}
submitFilter={() => update(1)}
/>
)
}
>
<Button
style={{
paddingLeft: 4, paddingRight: 4, marginLeft: 4,
color: !!params.SearchURL ? undefined : "gray",
}}
type={!!params.SearchURL ? "primary" : "link"} size={"small"}
icon={<SearchOutlined/>}
/>
</Popover>
</div>
)
},
cellRender: ({rowData, dataKey, ...props}: any) => {
if (rowData.IsPlaceholder) {
return <div style={{color: "#888585"}}>{"滚轮上滑刷新..."}</div>
}
return (
<div style={{width: "100%", display: "flex"}}>
<div className='resize-ellipsis' title={rowData.Url}>
{!params.SearchURL ? (
rowData.Url
) : (
rowData.Url
)}
</div>
</div>
)
},
width: 600
},
{
dataKey: "HtmlTitle",
width: 120,
resizable: true,
headRender: () => {
return "Title"
},
cellRender: ({rowData, dataKey, ...props}: any) => {
return rowData[dataKey] ? rowData[dataKey] : ""
}
},
{
dataKey: "Tags",
width: 120,
resizable: true,
headRender: () => {
return "Tags"
},
cellRender: ({rowData, dataKey, ...props}: any) => {
return rowData[dataKey] ? (
`${rowData[dataKey]}`.split("|").filter(i => !i.startsWith("YAKIT_COLOR_")).join(", ")
) : ""
}
},
{
dataKey: "IPAddress",
width: 140, resizable: true,
headRender: () => {
return "IP"
},
cellRender: ({rowData, dataKey, ...props}: any) => {
return rowData[dataKey] ? rowData[dataKey] : ""
}
},
{
dataKey: "BodyLength",
width: 120,
sortable: true,
headRender: () => {
return (
<div style={{display: "inline-block", position: "relative"}}>
响应长度
<Popover
placement='bottom'
trigger='click'
content={
params &&
setParams && (
<HTTLFlowFilterDropdownForms
label={"是否存在Body?"}
params={params}
setParams={setParams}
filterName={"HaveBody"}
pureBool={true}
submitFilter={() => update(1)}
/>
)
}
>
<Button
style={{
paddingLeft: 4, paddingRight: 4, marginLeft: 4,
color: !!params.HaveBody ? undefined : "gray",
}}
type={!!params.HaveBody ? "primary" : "link"} size={"small"}
icon={<SearchOutlined/>}
/>
</Popover>
</div>
)
},
cellRender: ({rowData, dataKey, ...props}: any) => {
return (
<div style={{width: 100}}>
{/* 1M 以上的话,是红色*/}
{rowData.BodyLength !== -1 ?
(<div style={{color: rowData.BodyLength > 1000000 ? "red" : undefined}}>
{rowData.BodySizeVerbose
? rowData.BodySizeVerbose
: rowData.BodyLength}
</div>)
:
(<div></div>)
}
</div>
)
}
},
// {
// dataKey: "UrlLength",
// width: 90,
// headRender: () => {
// return "URL 长度"
// },
// cellRender: ({rowData, dataKey, ...props}: any) => {
// const len = (rowData.Url || "").length
// return len > 0 ? <div>{len}</div> : "-"
// }
// },
{
dataKey: "GetParamsTotal",
width: 65,
align: "center",
headRender: () => {
return (
<div
style={{display: "flex", justifyContent: "space-between"}}
>
参数
<Popover
placement='bottom'
trigger='click'
content={
params &&
setParams && (
<HTTLFlowFilterDropdownForms
label={"过滤是否存在基础参数"}
params={params}
setParams={setParams}
filterName={"HaveCommonParams"}
pureBool={true}
submitFilter={() => update(1)}
/>
)
}
>
<Button
style={{
paddingLeft: 4, paddingRight: 4, marginLeft: 4,
color: !!params.HaveCommonParams ? undefined : "gray",
}}
type={!!params.HaveCommonParams ? "primary" : "link"} size={"small"}
icon={<SearchOutlined/>}
/>
</Popover>
</div>
)
},
cellRender: ({rowData, dataKey, ...props}: any) => {
return (
<Space>
{(rowData.GetParamsTotal > 0 ||
rowData.PostParamsTotal > 0) && <CheckOutlined/>}
</Space>
)
}
},
{
dataKey: "ContentType",
resizable: true, width: 80,
headRender: () => {
return "响应类型"
},
cellRender: ({rowData, dataKey, ...props}: any) => {
let contentTypeFixed = rowData.ContentType.split(";")
.map((el: any) => el.trim())
.filter((i: any) => !i.startsWith("charset"))
.join(",") || "-"
if (contentTypeFixed.includes("/")) {
const contentTypeFixedNew = contentTypeFixed.split("/").pop()
if (!!contentTypeFixedNew) {
contentTypeFixed = contentTypeFixedNew
}
}
return (
<div>
{contentTypeFixed === "null" ? "" : contentTypeFixed}
</div>
)
}
},
{
dataKey: "UpdatedAt",
sortable: true,
width: 110,
headRender: () => {
return "请求时间"
},
cellRender: ({rowData, dataKey, ...props}: any) => {
return <Tooltip
title={rowData[dataKey] === 0 ? "" : formatTimestamp(rowData[dataKey])}
>
{rowData[dataKey] === 0 ? "" : formatTime(rowData[dataKey])}
</Tooltip>
}
},
{
dataKey: "operate",
width: 90,
headRender: () => "操作",
cellRender: ({rowData}: any) => {
if (!rowData.Hash) return <></>
return (
<a
onClick={(e) => {
let m = showDrawer({
width: "80%",
content: onExpandHTTPFlow(
rowData,
() => m.destroy()
)
})
}}
>
详情
</a>
)
}
}
]}
data={autoReload ? data : [TableFirstLinePlaceholder].concat(data)}
autoHeight={tableContentHeight <= 0}
height={tableContentHeight}
sortFilter={sortFilter}
renderRow={(children: ReactNode, rowData: any) => {
if (rowData)
return (
<div
id='http-flow-row'
ref={(node) => {
const color =
rowData.Hash === selected?.Hash ?
"rgba(78, 164, 255, 0.4)" :
rowData.Tags.indexOf("YAKIT_COLOR") > -1 ?
TableRowColor(rowData.Tags.split("|").pop().split('_').pop().toUpperCase()) :
"#ffffff"
if (node) {
if (color) node.style.setProperty("background-color", color, "important")
else node.style.setProperty("background-color", "#ffffff")
}
}}
style={{height: "100%"}}
>
{children}
</div>
)
return children
}}
onRowContextMenu={(rowData: HTTPFlow | any, event: React.MouseEvent) => {
if (rowData) {
setSelected(rowData);
}
showByCursorMenu(
{
content: [
{
title: '发送到 Web Fuzzer',
onClick: () => {
ipcRenderer.invoke("send-to-tab", {
type: "fuzzer",
data: {
isHttps: rowData.IsHTTPS,
request: new Buffer(rowData.Request).toString("utf8")
}
})
}
},
{
title: '发送到 数据包扫描',
onClick: () => {
ipcRenderer
.invoke("GetHTTPFlowByHash", {Hash: rowData.Hash})
.then((i: HTTPFlow) => {
ipcRenderer.invoke("send-to-packet-hack", {
request: i.Request,
ishttps: i.IsHTTPS,
response: i.Response
})
})
.catch((e: any) => {
failed(`Query Response failed: ${e}`)
})
}
},
{
title: '复制 URL',
onClick: () => {
callCopyToClipboard(rowData.Url)
},
},
{
title: '复制为 Yak PoC 模版', onClick: () => {
},
subMenuItems: [
{
title: "数据包 PoC 模版", onClick: () => {
const flow = rowData as HTTPFlow;
if (!flow) return;
generateYakCodeByRequest(flow.IsHTTPS, flow.Request, code => {
callCopyToClipboard(code)
}, RequestToYakCodeTemplate.Ordinary)
}
},
{
title: "批量检测 PoC 模版", onClick: () => {
const flow = rowData as HTTPFlow;
if (!flow) return;
generateYakCodeByRequest(flow.IsHTTPS, flow.Request, code => {
callCopyToClipboard(code)
}, RequestToYakCodeTemplate.Batch)
}
},
]
},
{
title: '标注颜色',
subMenuItems: availableColors.map(i => {
return {
title: i.title,
render: i.render,
onClick: () => {
const flow = rowData as HTTPFlow
if (!flow) {
return
}
const existedTags = flow.Tags ? flow.Tags.split("|").filter(i => !!i && !i.startsWith("YAKIT_COLOR_")) : []
existedTags.push(`YAKIT_COLOR_${i.color.toUpperCase()}`)
ipcRenderer.invoke("SetTagForHTTPFlow", {
Id: flow.Id, Hash: flow.Hash,
Tags: existedTags,
}).then(() => {
info(`设置 HTTPFlow 颜色成功`)
if (!autoReload) {
setData(data.map(item => {
if (item.Hash === flow.Hash) {
item.Tags = `YAKIT_COLOR_${i.color.toUpperCase()}`
return item
}
return item
}))
}
})
}
}
}),
onClick: () => {
}
},
{
title: '移除颜色',
onClick: () => {
const flow = rowData as HTTPFlow
if (!flow) return
const existedTags = flow.Tags ? flow.Tags.split("|").filter(i => !!i && !i.startsWith("YAKIT_COLOR_")) : []
existedTags.pop()
ipcRenderer.invoke("SetTagForHTTPFlow", {
Id: flow.Id, Hash: flow.Hash,
Tags: existedTags,
}).then(() => {
info(`清除 HTTPFlow 颜色成功`)
if (!autoReload) {
setData(data.map(item => {
if (item.Hash === flow.Hash) {
item.Tags = ""
return item
}
return item
}))
}
})
return
},
},
{
title: "发送到对比器", onClick: () => {
},
subMenuItems: [
{
title: '发送到对比器左侧',
onClick: () => {
setCompareLeft({
content: new Buffer(rowData.Request).toString("utf8"),
language: 'http'
})
},
disabled: [false, true, false][compareState]
},
{
title: '发送到对比器右侧',
onClick: () => {
setCompareRight({
content: new Buffer(rowData.Request).toString("utf8"),
language: 'http'
})
},
disabled: [false, false, true][compareState]
}
]
},
]
},
event.clientX,
event.clientY
)
}}
onRowClick={(rowDate: any) => {
if (!rowDate.Hash) return
if (rowDate.Hash !== selected?.Hash) {
setSelected(rowDate)
} else {
// setSelected(undefined)
}
}}
onScroll={(scrollX, scrollY) => {
setScrollY(scrollY)
// 防止无数据触发加载
if (data.length === 0 && !getAutoReload()) {
setAutoReload(true)
return
}
// 根据页面展示内容决定是否自动刷新
let contextHeight = (data.length + 1) * ROW_HEIGHT // +1 是要把表 title 算进去
let offsetY = scrollY + tableContentHeight;
if (contextHeight < tableContentHeight) {
setAutoReload(true)
return
}
setAutoReload(false)
// 向下刷新数据
if (contextHeight <= offsetY) {
setAutoReload(false)
scrollUpdateButt(tableContentHeight)
return
}
// 锁住滚轮
if (getLockedScroll() > 0 && getLockedScroll() >= scrollY) {
if (scrollY === getLockedScroll()) {
return
}
// scrollTableTo(getLockedScroll())
return
}
const toTop = scrollY <= 0;
if (toTop) {
lockScrollTimeout(ROW_HEIGHT, 600)
scrollUpdateTop()
}
}}
/>
</div>
// </AutoCard>
)
}
Example #27
Source File: MITMPage.tsx From yakit with GNU Affero General Public License v3.0 | 4 votes |
MITMPage: React.FC<MITMPageProp> = (props) => {
const [status, setStatus] = useState<"idle" | "hijacked" | "hijacking">("idle");
const [error, setError] = useState("");
const [host, setHost] = useState("127.0.0.1");
const [port, setPort] = useState(8083);
const [downstreamProxy, setDownstreamProxy] = useState<string>();
const [loading, setLoading] = useState(false);
const [caCerts, setCaCerts] = useState<CaCertData>({
CaCerts: new Buffer(""), LocalFile: "",
});
const [enableInitialPlugin, setEnableInitialPlugin] = useState(false);
// 存储修改前和修改后的包!
const [currentPacketInfo, setCurrentPacketInfo] = useState<{
currentPacket: Uint8Array,
currentPacketId: number,
isHttp: boolean
}>({currentPacketId: 0, currentPacket: new Buffer([]), isHttp: true});
const {currentPacket, currentPacketId, isHttp} = currentPacketInfo;
const clearCurrentPacket = () => {
setCurrentPacketInfo({currentPacketId: 0, currentPacket: new Buffer([]), isHttp: true})
}
const [modifiedPacket, setModifiedPacket] = useState<Uint8Array>(new Buffer([]));
// 自动转发 与 劫持响应的自动设置
const [autoForward, setAutoForward, getAutoForward] = useGetState<"manual" | "log" | "passive">("log");
const isManual = autoForward === "manual";
const [hijackAllResponse, setHijackAllResponse] = useState(false); // 劫持所有请求
const [allowHijackCurrentResponse, setAllowHijackCurrentResponse] = useState(false); // 仅劫持一个请求
const [initialed, setInitialed] = useState(false);
const [forResponse, setForResponse] = useState(false);
const [haveSideCar, setHaveSideCar] = useState(true);
const [urlInfo, setUrlInfo] = useState("监听中...")
const [ipInfo, setIpInfo] = useState("")
// 设置初始化启动的插件
const [defaultPlugins, setDefaultPlugins] = useState<string[]>([]);
// yakit log message
const [logs, setLogs] = useState<ExecResultLog[]>([]);
const latestLogs = useLatest<ExecResultLog[]>(logs);
const [_, setLatestStatusHash, getLatestStatusHash] = useGetState("");
const [statusCards, setStatusCards] = useState<StatusCardProps[]>([])
// filter 过滤器
const [mitmFilter, setMITMFilter] = useState<MITMFilterSchema>();
// 内容替代模块
const [replacers, setReplacers] = useState<MITMContentReplacerRule[]>([]);
// mouse
const mouseState = useMouse();
// 操作系统类型
const [system, setSystem] = useState<string>()
useEffect(() => {
ipcRenderer.invoke('fetch-system-name').then((res) => setSystem(res))
}, [])
useEffect(() => {
// 设置 MITM 初始启动插件选项
getValue(CONST_DEFAULT_ENABLE_INITIAL_PLUGIN).then(a => {
setEnableInitialPlugin(!!a)
})
}, [])
// 用于接受后端传回的信息
useEffect(() => {
setInitialed(false)
// 用于前端恢复状态
ipcRenderer.invoke("mitm-have-current-stream").then(data => {
const {haveStream, host, port} = data;
if (haveStream) {
setStatus("hijacking")
setHost(host);
setPort(port);
}
}).finally(() => {
recover()
setTimeout(() => setInitialed(true), 500)
})
// 用于启动 MITM 开始之后,接受开始成功之后的第一个消息,如果收到,则认为说 MITM 启动成功了
ipcRenderer.on("client-mitm-start-success", () => {
setStatus("hijacking")
setTimeout(() => {
setLoading(false)
}, 300)
})
// 用于 MITM 的 Message (YakitLog)
const messages: ExecResultLog[] = [];
const statusMap = new Map<string, StatusCardProps>();
let lastStatusHash = '';
ipcRenderer.on("client-mitm-message", (e, data: ExecResult) => {
let msg = ExtractExecResultMessage(data);
if (msg !== undefined) {
// logHandler.logs.push(msg as ExecResultLog)
// if (logHandler.logs.length > 25) {
// logHandler.logs.shift()
// }
const currentLog = msg as ExecResultLog;
if (currentLog.level === "feature-status-card-data") {
lastStatusHash = `${currentLog.timestamp}-${currentLog.data}`
try {
// 解析 Object
const obj = JSON.parse(currentLog.data)
const {id, data} = obj;
if (!data) {
statusMap.delete(`${id}`)
} else {
statusMap.set(`${id}`, {Data: data, Id: id, Timestamp: currentLog.timestamp})
}
} catch (e) {
}
return
}
messages.push(currentLog)
if (messages.length > 25) {
messages.shift()
}
}
})
// let currentFlow: HTTPFlow[] = []
ipcRenderer.on("client-mitm-history-update", (e: any, data: any) => {
// currentFlow.push(data.historyHTTPFlow as HTTPFlow)
//
// if (currentFlow.length > 30) {
// currentFlow = [...currentFlow.slice(0, 30)]
// }
// setFlows([...currentFlow])
})
ipcRenderer.on("client-mitm-error", (e, msg) => {
if (!msg) {
info("MITM 劫持服务器已关闭")
} else {
failed("MITM 劫持服务器异常或被关闭")
Modal.error({
mask: true, title: "启动 MITM 服务器 ERROR!",
content: <>{msg}</>
})
}
ipcRenderer.invoke("mitm-stop-call")
setError(`${msg}`)
setStatus("idle")
setTimeout(() => {
setLoading(false)
}, 300)
});
ipcRenderer.on("client-mitm-filter", (e, msg) => {
ipcRenderer
.invoke("get-value", DefaultMitmFilter)
.then((res: any) => {
if (res) {
const filter = {
includeSuffix: res.includeSuffix,
excludeMethod: res.excludeMethod,
excludeSuffix: res.excludeSuffix,
includeHostname: res.includeHostname,
excludeHostname: res.excludeHostname,
excludeContentTypes: res.excludeContentTypes,
}
setMITMFilter(filter)
ipcRenderer.invoke("mitm-filter", {
updateFilter: true, ...filter
})
} else {
setMITMFilter({
includeSuffix: msg.includeSuffix,
excludeMethod: msg.excludeMethod,
excludeSuffix: msg.excludeSuffix,
includeHostname: msg.includeHostname,
excludeHostname: msg.excludeHostname,
excludeContentTypes: msg.excludeContentTypes,
})
}
})
})
const updateLogs = () => {
if (latestLogs.current.length !== messages.length) {
setLogs([...messages])
return
}
if (latestLogs.current.length > 0 && messages.length > 0) {
if (latestLogs.current[0].data !== messages[0].data) {
setLogs([...messages])
return
}
}
if (getLatestStatusHash() !== lastStatusHash) {
setLatestStatusHash(lastStatusHash)
const tmpCurrent: StatusCardProps[] = [];
statusMap.forEach((value, key) => {
tmpCurrent.push(value)
})
setStatusCards(tmpCurrent.sort((a, b) => a.Id.localeCompare(b.Id)))
}
}
updateLogs()
let id = setInterval(() => {
updateLogs()
}, 1000)
return () => {
clearInterval(id);
ipcRenderer.removeAllListeners("client-mitm-error")
// ipcRenderer.invoke("mitm-close-stream")
}
}, [])
useEffect(() => {
if (hijackAllResponse && currentPacketId > 0) {
allowHijackedResponseByRequest(currentPacketId)
}
}, [hijackAllResponse, currentPacketId])
useEffect(() => {
ipcRenderer.on("client-mitm-hijacked", forwardHandler);
return () => {
ipcRenderer.removeAllListeners("client-mitm-hijacked")
}
}, [autoForward])
useEffect(() => {
ipcRenderer.invoke("mitm-auto-forward", !isManual).finally(() => {
console.info(`设置服务端自动转发:${!isManual}`)
})
}, [autoForward])
useEffect(() => {
ipcRenderer.on("client-mitm-content-replacer-update", (e, data: MITMResponse) => {
setReplacers(data?.replacers || [])
return
});
return () => {
ipcRenderer.removeAllListeners("client-mitm-content-replacer-update")
}
}, [])
useEffect(() => {
if (currentPacketId <= 0 && status === "hijacked") {
recover()
const id = setInterval(() => {
recover()
}, 500)
return () => {
clearInterval(id)
}
}
}, [currentPacketId])
useEffect(() => {
ipcRenderer.invoke("DownloadMITMCert", {}).then((data: CaCertData) => {
setCaCerts(data)
})
}, [])
const addr = `http://${host}:${port}`;
// 自动转发劫持,进行的操作
const forwardHandler = useMemoizedFn((e: any, msg: MITMResponse) => {
setMITMFilter({
includeSuffix: msg.includeSuffix,
excludeMethod: msg.excludeMethod,
excludeSuffix: msg.excludeSuffix,
includeHostname: msg.includeHostname,
excludeHostname: msg.excludeHostname,
excludeContentTypes: msg.excludeContentTypes,
})
// passive 模式是 mitm 插件模式
// 在这个模式下,应该直接转发,不应该操作数据包
// if (passiveMode) {
// if (msg.forResponse) {
// forwardResponse(msg.responseId || 0)
// } else {
// forwardRequest(msg.id || 0)
// }
// return
// }
if (msg.forResponse) {
if (!msg.response || !msg.responseId) {
failed("BUG: MITM 错误,未能获取到正确的 Response 或 Response ID")
return
}
if (!isManual) {
forwardResponse(msg.responseId || 0)
if (!!currentPacket) {
clearCurrentPacket()
}
} else {
setForResponse(true)
setStatus("hijacked")
setCurrentPacketInfo({
currentPacket: msg.response,
currentPacketId: msg.responseId,
isHttp: msg.isHttps
})
// setCurrentPacket(new Buffer(msg.response).toString("utf8"))
// setCurrentPacketId(msg.responseId || 0);
}
} else {
if (msg.request) {
if (!isManual) {
forwardRequest(msg.id)
if (!!currentPacket) {
clearCurrentPacket()
}
// setCurrentPacket(String.fromCharCode.apply(null, msg.request))
} else {
setStatus("hijacked")
setForResponse(false)
// setCurrentPacket(msg.request)
// setCurrentPacketId(msg.id)
setCurrentPacketInfo({currentPacket: msg.request, currentPacketId: msg.id, isHttp: msg.isHttps})
setUrlInfo(msg.url)
ipcRenderer.invoke("fetch-url-ip", msg.url.split('://')[1].split('/')[0]).then((res) => {
setIpInfo(res)
})
}
}
}
})
// 这个 Forward 主要用来转发修改后的内容,同时可以转发请求和响应
const forward = useMemoizedFn(() => {
// ID 不存在
if (!currentPacketId) {
return
}
setLoading(true);
setStatus("hijacking");
setAllowHijackCurrentResponse(false)
setForResponse(false)
if (forResponse) {
ipcRenderer.invoke("mitm-forward-modified-response", modifiedPacket, currentPacketId).finally(() => {
clearCurrentPacket()
setTimeout(() => setLoading(false))
})
} else {
ipcRenderer.invoke("mitm-forward-modified-request", modifiedPacket, currentPacketId).finally(() => {
clearCurrentPacket()
setTimeout(() => setLoading(false))
})
}
})
const recover = useMemoizedFn(() => {
ipcRenderer.invoke("mitm-recover").then(() => {
// success("恢复 MITM 会话成功")
})
})
const start = useMemoizedFn(() => {
setLoading(true)
setError("")
ipcRenderer.invoke("mitm-start-call", host, port, downstreamProxy).catch((e: any) => {
notification["error"]({message: `启动中间人劫持失败:${e}`})
})
})
const stop = useMemoizedFn(() => {
setLoading(true)
ipcRenderer.invoke("mitm-stop-call").then(() => {
setStatus("idle")
}).catch((e: any) => {
notification["error"]({message: `停止中间人劫持失败:${e}`})
}).finally(() => setTimeout(() => {
setLoading(false)
}, 300))
})
const hijacking = useMemoizedFn(() => {
// setCurrentPacket(new Buffer([]));
clearCurrentPacket()
setLoading(true);
setStatus("hijacking");
})
function getCurrentId() {
return currentPacketId
}
const downloadCert = useMemoizedFn(() => {
return <Tooltip title={'请先下载 SSL/TLS 证书'}>
<Button
type={"link"}
style={{padding: '4px 6px'}}
onClick={() => {
const text = `wget -e use_proxy=yes -e http_proxy=${addr} http://download-mitm-cert.yaklang.io -O yakit-mitm-cert.pem`
showModal({
title: "下载 SSL/TLS 证书以劫持 HTTPS",
width: "50%",
content: <Space direction={"vertical"} style={{width: "100%"}}>
<AutoCard
title={"证书配置"}
extra={<Button
type={"link"}
onClick={() => {
saveABSFileToOpen("yakit证书.crt.pem", caCerts.CaCerts)
// openABSFileLocated(caCerts.LocalFile)
}}
>
下载到本地并打开
</Button>} size={"small"} bodyStyle={{padding: 0}}>
<div style={{height: 360}}>
<YakEditor bytes={true}
valueBytes={caCerts.CaCerts}
/>
</div>
</AutoCard>
<Alert message={<Space>
在设置代理后访问:<CopyableField text={"http://download-mitm-cert.yaklang.io"}/> 可自动下载证书
</Space>}/>
</Space>
})
}}
>HTTPS 证书配置</Button>
</Tooltip>
})
const contentReplacer = useMemoizedFn(() => {
return <Button
type={"link"} style={{padding: `4px 6px`}}
onClick={() => {
let m = showDrawer({
placement: "top", height: "50%",
content: (
<MITMContentReplacer
rules={replacers}
onSaved={rules => {
setReplacers(rules)
m.destroy()
}}/>
),
maskClosable: false,
})
}}
>
匹配/标记/替换
</Button>
})
const setFilter = useMemoizedFn(() => {
return <Button type={"link"} style={{padding: '4px 6px'}}
onClick={() => {
let m = showDrawer({
placement: "top", height: "50%",
content: <>
<MITMFilters
filter={mitmFilter}
onFinished={(filter) => {
setMITMFilter({...filter})
m.destroy()
}}/>
</>
});
}}
>过滤器</Button>
})
const handleAutoForward = useMemoizedFn((e: "manual" | "log" | "passive") => {
if (!isManual) {
info("切换为劫持自动放行模式(仅记录)")
setHijackAllResponse(false)
} else {
info("切换为手动放行模式(可修改劫持)")
}
setAutoForward(e)
if (currentPacket && currentPacketId) {
forward()
}
})
const execFuzzer = useMemoizedFn((value: string) => {
ipcRenderer.invoke("send-to-tab", {
type: "fuzzer",
data: {isHttps: currentPacketInfo.isHttp, request: value}
})
})
const execPlugin = useMemoizedFn((value: string) => {
ipcRenderer.invoke("send-to-packet-hack", {
request: currentPacketInfo.currentPacket,
ishttps: currentPacketInfo.isHttp
})
})
const shiftAutoForwardHotkey = useHotkeys('ctrl+t', () => {
handleAutoForward(isManual ? "manual" : "log")
}, [autoForward])
if (!initialed) {
return <div style={{textAlign: "center", paddingTop: 120}}>
<Spin spinning={true} tip={"正在初始化 MITM"}/>
</div>
}
return <div style={{height: "100%", width: "100%"}}>
{(() => {
switch (status) {
case "idle":
return <Spin spinning={loading}>
<Form
style={{marginTop: 40}}
onSubmitCapture={e => {
e.preventDefault()
start()
if (enableInitialPlugin) {
enableMITMPluginMode(defaultPlugins).then(() => {
info("被动扫描插件模式已启动")
})
}
}}
layout={"horizontal"} labelCol={{span: 7}}
wrapperCol={{span: 13}}
>
<Item label={"劫持代理监听主机"}>
<Input value={host} onChange={e => setHost(e.target.value)}/>
</Item>
<Item label={"劫持代理监听端口"}>
<InputNumber value={port} onChange={e => setPort(e)}/>
</Item>
{/*<SwitchItem label={"启动 MITM 插件"} size={"small"} setValue={e => {*/}
{/* setEnableInitialPlugin(e)*/}
{/* if (e) {*/}
{/* saveValue(CONST_DEFAULT_ENABLE_INITIAL_PLUGIN, "true")*/}
{/* } else {*/}
{/* saveValue(CONST_DEFAULT_ENABLE_INITIAL_PLUGIN, "")*/}
{/* }*/}
{/*}} value={enableInitialPlugin}/>*/}
<Item label={"选择插件"} colon={true}>
<div style={{height: 200, maxWidth: 420}}>
<SimplePluginList
disabled={!enableInitialPlugin}
bordered={true}
initialSelected={defaultPlugins}
onSelected={(list: string[]) => {
setDefaultPlugins(list)
}} pluginTypes={"mitm,port-scan"}
verbose={<div>MITM 与 端口扫描插件</div>}/>
</div>
</Item>
<Item label={"下游代理"} help={"为经过该 MITM 代理的请求再设置一个代理,通常用于访问中国大陆无法访问的网站或访问特殊网络/内网,也可用于接入被动扫描"}>
<Input value={downstreamProxy} onChange={e => setDownstreamProxy(e.target.value)}/>
</Item>
<Item label={"内容规则"} help={"使用规则进行匹配、替换、标记、染色,同时配置生效位置"}>
<Space>
<Button
onClick={() => {
let m = showDrawer({
placement: "top", height: "50%",
content: (
<MITMContentReplacerViewer/>
),
maskClosable: false,
})
}}
>已有规则</Button>
<Button type={"link"} onClick={() => {
const m = showModal({
title: "从 JSON 中导入",
width: "60%",
content: (
<>
<MITMContentReplacerImport onClosed={() => {
m.destroy()
}}/>
</>
)
})
}}>从 JSON 导入</Button>
<Button type={"link"} onClick={() => {
showModal({
title: "导出配置 JSON",
width: "50%",
content: (
<>
<MITMContentReplacerExport/>
</>
)
})
}}>导出为 JSON</Button>
</Space>
</Item>
<Item label={" "} colon={false}>
<Space>
<Button type={"primary"} htmlType={"submit"}>
劫持启动
</Button>
<Divider type={"vertical"}/>
<Checkbox
checked={enableInitialPlugin}
onChange={node => {
const e = node.target.checked;
setEnableInitialPlugin(e)
if (e) {
saveValue(CONST_DEFAULT_ENABLE_INITIAL_PLUGIN, "true")
} else {
saveValue(CONST_DEFAULT_ENABLE_INITIAL_PLUGIN, "")
}
}}
>
插件自动加载
</Checkbox>
</Space>
</Item>
</Form>
</Spin>
case "hijacking":
case "hijacked":
return <div id={"mitm-hijacking-container"} ref={shiftAutoForwardHotkey as Ref<any>} tabIndex={-1}
style={{marginLeft: 12, marginRight: 12, height: "100%"}}>
<Row gutter={14} style={{height: "100%"}}>
<Col span={haveSideCar ? 24 : 24}
style={{display: "flex", flexDirection: "column", height: "100%"}}>
<PageHeader
className="mitm-header-title"
title={'劫持 HTTP Request'} subTitle={`http://${host}:${port}`}
style={{marginRight: 0, paddingRight: 0, paddingTop: 0, paddingBottom: 8}}
extra={
<Space>
<ChromeLauncherButton host={host} port={port}/>
{contentReplacer()}
{setFilter()}
{downloadCert()}
<Button danger={true} type={"link"}
onClick={() => {
stop()
setUrlInfo("监听中...")
setIpInfo("")
}} icon={<PoweroffOutlined/>}
/>
</Space>}>
<Row>
<Col span={12}>
<div style={{width: "100%", textAlign: "left"}}>
<Space>
<Button
type={"primary"}
disabled={status === "hijacking"}
onClick={() => {
forward()
}}>提交数据</Button>
<Button
disabled={status === "hijacking"}
danger={true}
onClick={() => {
hijacking()
if (forResponse) {
dropResponse(currentPacketId).finally(() => {
setTimeout(() => {
setLoading(false)
}, 300)
})
} else {
dropRequest(currentPacketId).finally(() => {
setTimeout(() => setLoading(false), 300)
})
}
setUrlInfo("监听中...")
setIpInfo("")
}}>丢弃请求</Button>
{
(!forResponse && !!currentPacket) && // 劫持到的请求有内容
status === "hijacked" && // 劫持到的状态是 hijacked
!hijackAllResponse && // 如果已经设置了劫持所有请求,就不展示了
<Button
disabled={allowHijackCurrentResponse}
type={allowHijackCurrentResponse ? "primary" : "default"}
onClick={() => {
if (!allowHijackCurrentResponse) {
allowHijackedResponseByRequest(currentPacketId)
setAllowHijackCurrentResponse(true)
} else {
setAllowHijackCurrentResponse(false)
}
}}>
劫持响应 {
allowHijackCurrentResponse &&
<CheckOutlined/>
}
</Button>}
</Space>
</div>
</Col>
<Col span={12}>
<div style={{width: "100%", textAlign: "right"}}>
<Space>
{isManual && <div>
<span style={{marginRight: 4}}>劫持响应:</span>
<Checkbox checked={hijackAllResponse} onClick={e => {
if (!hijackAllResponse) {
info("劫持所有响应内容")
} else {
info("仅劫持请求")
}
setHijackAllResponse(!hijackAllResponse)
}}/>
</div>}
<SelectOne
data={[
{text: "手动劫持", value: "manual"},
{text: "自动放行", value: "log"},
{text: "被动日志", value: "passive"},
]}
value={autoForward}
formItemStyle={{marginBottom: 0}}
setValue={(e) => {
ipcRenderer.invoke("mitm-filter", {updateFilter: true, ...mitmFilter})
handleAutoForward(e)
}}
/>
</Space>
</div>
</Col>
</Row>
<Row>
<Col span={12}>
<div style={{
width: "100%", textAlign: "left", height: '100%',
display: 'flex'
}}>
{!isManual &&
<Text style={{alignSelf: 'center'}}>
{`目标:自动放行中...`}</Text>}
{autoForward === "manual" &&
<>
<Text title={urlInfo} ellipsis={true} style={{
alignSelf: 'center',
maxWidth: 300
}}>{status === 'hijacking' ? '目标:监听中...' : `目标:${urlInfo}`}</Text>
{ipInfo && status !== 'hijacking' &&
<Tag
color='green'
title={ipInfo}
style={{
marginLeft: 5,
alignSelf: "center",
maxWidth: 140,
cursor: "pointer"
}}
>
{`${ipInfo}`}
<CopyToClipboard
text={`${ipInfo}`}
onCopy={(text, ok) => {
if (ok) success("已复制到粘贴板")
}}
>
<CopyOutlined style={{marginLeft: 5}}/>
</CopyToClipboard>
</Tag>
}
</>
}
</div>
</Col>
<Col span={12}>
<div style={{width: "100%", textAlign: "right"}}>
<Button
type={"link"} onClick={() => recover()}
icon={<ReloadOutlined/>}
>恢复请求</Button>
</div>
</Col>
</Row>
</PageHeader>
<div style={{flex: 1, overflowY: 'hidden'}}>
{/*<Spin wrapperClassName={"mitm-loading-spin"} spinning={status === "hijacking"}>*/}
<div style={{height: "100%"}}>
<ResizeBox
isVer={false}
firstNode={(
<MITMPluginList
proxy={`http://${host}:${port}`}
downloadCertNode={downloadCert}
setFilterNode={setFilter}
onExit={() => {
stop()
}}
onSubmitScriptContent={e => {
ipcRenderer.invoke("mitm-exec-script-content", e)
}}
onSubmitYakScriptId={(id: number, params: YakExecutorParam[]) => {
info(`加载 MITM 插件[${id}]`)
ipcRenderer.invoke("mitm-exec-script-by-id", id, params)
}}
/>
// <MITMPluginOperator />
)}
firstMinSize={"330px"}
secondMinSize={"340px"}
firstRatio={"330px"}
secondNode={(
<AutoCard
style={{margin: 0, padding: 0}}
bodyStyle={{margin: 0, padding: 0, overflowY: "hidden"}}
>
{autoForward === "log" && (
<MITMPluginCard
onSubmitScriptContent={(e) => {
ipcRenderer.invoke("mitm-exec-script-content", e)
}}
onSubmitYakScriptId={(
id: number,
params: YakExecutorParam[]
) => {
info(`加载 MITM 插件[${id}]`)
ipcRenderer.invoke("mitm-exec-script-by-id", id, params)
}}
/>
)}
{autoForward === "manual" && (
<HTTPPacketEditor
originValue={currentPacket}
noHeader={true}
bordered={false}
onChange={setModifiedPacket}
noPacketModifier={true}
readOnly={status === "hijacking"}
refreshTrigger={
(forResponse ? `rsp` : `req`) + `${currentPacketId}`
}
actions={[
// {
// id: "send-to-scan-packet", label: "发送到数据包扫描器",
// run: e => {
// // console.info(mouseState)
// scanPacket(mouseState, false, "GET / HTTP/1.1\r\nHost: www.baidu.com", "")
// }, contextMenuGroupId: "Scanners",
// },
...(forResponse
? [
{
id: "trigger-auto-hijacked",
label: "切换为自动劫持模式",
keybindings: [
monaco.KeyMod.Shift |
(system === "Darwin" ? monaco.KeyMod.WinCtrl : monaco.KeyMod.CtrlCmd) |
monaco.KeyCode.KEY_T
],
run: () => {
handleAutoForward(getAutoForward() === "manual" ? "log" : "manual")
},
contextMenuGroupId: "Actions"
},
{
id: "forward-response",
label: "放行该 HTTP Response",
run: function () {
forward()
// hijacking()
// forwardResponse(getCurrentId()).finally(() => {
// setTimeout(() => setLoading(false), 300)
// })
},
contextMenuGroupId: "Actions"
},
{
id: "drop-response",
label: "丢弃该 HTTP Response",
run: function () {
hijacking()
dropResponse(getCurrentId()).finally(
() => {
setTimeout(
() => setLoading(false),
300
)
}
)
},
contextMenuGroupId: "Actions"
}
]
: [
{
id: "trigger-auto-hijacked",
label: "切换为自动劫持模式",
keybindings: [
monaco.KeyMod.Shift |
(system === "Darwin" ? monaco.KeyMod.WinCtrl : monaco.KeyMod.CtrlCmd) |
monaco.KeyCode.KEY_T
],
run: () => {
handleAutoForward(getAutoForward() === "manual" ? "log" : "manual")
},
contextMenuGroupId: "Actions"
},
{
id: "send-to-fuzzer",
label: "发送到 Web Fuzzer",
keybindings: [
monaco.KeyMod.Shift |
(system === "Darwin" ? monaco.KeyMod.WinCtrl : monaco.KeyMod.CtrlCmd) |
monaco.KeyCode.KEY_R
],
run: function (StandaloneEditor: any) {
execFuzzer(StandaloneEditor.getModel().getValue())
},
contextMenuGroupId: "Actions"
},
{
id: "send-to-plugin",
label: "发送到 数据包扫描",
keybindings: [
monaco.KeyMod.Shift |
(system === "Darwin" ? monaco.KeyMod.WinCtrl : monaco.KeyMod.CtrlCmd) |
monaco.KeyCode.KEY_E
],
run: function (StandaloneEditor: any) {
if (!StandaloneEditor.getModel().getValue()) return
execPlugin(StandaloneEditor.getModel().getValue())
},
contextMenuGroupId: "Actions"
},
{
id: "forward-response",
label: "放行该 HTTP Request",
keybindings: [
monaco.KeyMod.Shift |
(system === "Darwin" ? monaco.KeyMod.WinCtrl : monaco.KeyMod.CtrlCmd) |
monaco.KeyCode.KEY_F
],
run: function () {
forward()
// hijacking()
// forwardRequest(getCurrentId()).finally(() => {
// setTimeout(() => setLoading(false), 300)
// })
},
contextMenuGroupId: "Actions"
},
{
id: "drop-response",
label: "丢弃该 HTTP Request",
run: function () {
hijacking()
dropRequest(getCurrentId()).finally(
() => {
setTimeout(
() => setLoading(false),
300
)
}
)
},
contextMenuGroupId: "Actions"
},
{
id: "hijack-current-response",
label: "劫持该 Request 对应的响应",
run: function () {
allowHijackedResponseByRequest(
getCurrentId()
)
},
contextMenuGroupId: "Actions"
}
])
]}
/>
)}
{autoForward === "passive" && (
<MITMPluginLogViewer
messages={logs} status={statusCards}
/>
)}
</AutoCard>
)}
/>
</div>
{/*</Spin>*/}
</div>
</Col>
</Row>
</div>
default:
return <div/>
}
})()}
</div>
}
Example #28
Source File: FunctionDebuggerSidebar.tsx From next-basics with GNU General Public License v3.0 | 4 votes |
export function FunctionDebuggerSidebar({
functionName,
functionModified,
activeTab,
tests,
onActiveTabSwitch,
onRunAllTests,
onAddTest,
}: FunctionDebuggerSidebarProps): React.ReactElement {
const [currentTab, setCurrentTab] = useState<string>(activeTab ?? "function");
useEffect(() => {
setCurrentTab(activeTab ?? "function");
}, [activeTab]);
const groups: SidebarGroup[] = useMemo(() => {
const refinedTests = Array.isArray(tests) ? tests : [];
return [
{
label: "Function",
value: "function",
items: [
{
label: functionName,
value: "function",
icon: <CodeOutlined />,
modified: functionModified,
},
],
},
{
label: "Debug",
value: "debug",
items: [
{
label: "Debug",
value: "debug",
icon: <BugOutlined />,
},
],
},
{
label: `Tests (${refinedTests.length})`,
value: "tests",
items: refinedTests.map((test, index) => ({
label: test.name ?? `Case ${index + 1}`,
value: `test:${String(index)}`,
modified: test.testModified,
...(test.testMatched
? {
icon: <CheckOutlined />,
className: styles.matched,
}
: test.testMatched === false
? {
icon: <CloseOutlined />,
className: styles.notMatched,
}
: {
icon: <QuestionOutlined />,
}),
})),
},
];
}, [functionModified, functionName, tests]);
const switchActiveTab = useCallback(
(tab: string) => {
if (currentTab !== tab) {
setCurrentTab(tab);
onActiveTabSwitch?.(tab);
}
},
[currentTab, onActiveTabSwitch]
);
return (
<div
className={`${styles.sidebarContainer} ${sharedStyles.customScrollbarContainer}`}
data-override-theme="dark"
>
<ul className={styles.sidebarGroups}>
{groups.map((group) => (
<li key={group.label}>
<div className={styles.sidebarGroupLabel}>
<span className={styles.groupText}>{group.label}</span>
{group.value === "tests" && (
<div className={styles.groupIconContainer}>
{group.items.length > 0 && (
<span
className={styles.groupIcon}
title="Run All Tests"
onClick={onRunAllTests}
>
<PlayCircleOutlined />
</span>
)}
<span
className={styles.groupIcon}
title="Add Test"
onClick={onAddTest}
>
<PlusCircleOutlined />
</span>
</div>
)}
</div>
<ul className={styles.sidebarItems}>
{group.items.map((item) => (
<li
key={item.label}
className={classNames({
[styles.active]: item.value === currentTab,
})}
onClick={() => switchActiveTab(item.value)}
>
<span className={classNames(styles.icon, item.className)}>
{item.icon}
</span>
<span className={styles.text}>{item.label}</span>
{item.modified && <span className={styles.modified}></span>}
</li>
))}
</ul>
</li>
))}
</ul>
</div>
);
}
Example #29
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>