react-hotkeys-hook#useHotkeys TypeScript Examples
The following examples show how to use
react-hotkeys-hook#useHotkeys.
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: HotKeys.tsx From react-pwa with MIT License | 6 votes |
function HotKeys() {
const [, themeActions] = useTheme();
const [, sidebarActions] = useSidebar();
const [isHotKeysDialogOpen, hotKeysDialogActions] = useHotKeysDialog();
// I would love to define all hotkeys in the config and loop it here and avoid this repetitive code.
// But the `react-hotkeys-hook` library, which we use to handle hotkeys provides only hook (`useHotkeys`).
// And as you know we can't use hooks inside loops (read "Rules of Hooks" - https://reactjs.org/docs/hooks-rules.html).
// There is always a workaround, but sometimes it's better to avoid premature and unnecessary optimizations :)
useHotkeys('alt+s', sidebarActions.toggle);
useHotkeys('alt+t', themeActions.toggle);
useHotkeys('alt+/', hotKeysDialogActions.toggle);
return (
<Dialog fullWidth maxWidth="xs" onClose={hotKeysDialogActions.close} open={isHotKeysDialogOpen}>
<DialogTitle>Hot Keys</DialogTitle>
<DialogContent>
<FlexBox alignItems="center" height={50} justifyContent="space-between">
<Typography>Toggle Theme</Typography>
<Button color="warning" variant="outlined" onClick={themeActions.toggle}>
alt + t
</Button>
</FlexBox>
<FlexBox alignItems="center" height={50} justifyContent="space-between">
<Typography>Toggle Sidebar</Typography>
<Button color="warning" variant="outlined" onClick={sidebarActions.toggle}>
alt + s
</Button>
</FlexBox>
<FlexBox alignItems="center" height={50} justifyContent="space-between">
<Typography>Toggle Hot Keys' Dialog</Typography>
<Button color="warning" variant="outlined" onClick={hotKeysDialogActions.toggle}>
alt + /
</Button>
</FlexBox>
</DialogContent>
</Dialog>
);
}
Example #2
Source File: useShortcuts.ts From rewind with MIT License | 6 votes |
export function useShortcuts() {
const { toggleClock, seekBackward, seekForward, increaseSpeed, decreaseSpeed } = useGameClockControls();
const { setHidden, hidden } = useModControls();
useHotkeys(generateKeyComboSimple(upKeys), () => increaseSpeed(), [increaseSpeed]);
useHotkeys(generateKeyComboSimple(downKeys), () => decreaseSpeed(), [decreaseSpeed]);
useHotkeys("space", () => toggleClock(), [toggleClock]);
useHotkeys(generateKeyCombo("shift", leftKeys), () => seekBackward(mediumJump), [seekBackward]);
useHotkeys(generateKeyCombo("shift", rightKeys), () => seekForward(mediumJump), [seekForward]);
useHotkeys(generateKeyCombo("ctrl", leftKeys), () => seekBackward(microscopeJump), [seekBackward]);
useHotkeys(generateKeyCombo("ctrl", rightKeys), () => seekForward(microscopeJump), [seekForward]);
useHotkeys(generateKeyComboSimple(leftKeys), () => seekBackward(frameJump), [seekBackward]);
useHotkeys(generateKeyComboSimple(rightKeys), () => seekForward(frameJump), [seekForward]);
// These have really bad collisions
// useHotkeys(`alt+${leftKey}`, () => seekBackward(frameJump), [seekBackward]);
// useHotkeys(`alt+${rightKey}`, () => seekForward(frameJump), [seekForward]);
// useHotkeys(`ctrl+${leftKey}`, () => seekBackward(largeJump), [seekBackward]);
// useHotkeys(`ctrl+${rightKey}`, () => seekForward(largeJump), [seekForward]);
useHotkeys("f", () => setHidden(!hidden), [hidden, setHidden]);
}
Example #3
Source File: index.tsx From admin with MIT License | 6 votes |
HotKeyAction = ({ label, hotKey, icon, onAction }: HotKeyActionProps) => {
useHotkeys(hotKey, onAction, {})
return (
<div className="flex items-center gap-2">
<span className="text-grey-0 inter-small-semibold">{label}</span>
<div className="inter-small-semibold text-grey-30 flex items-center justify-center w-[24px] h-[24px] rounded bg-grey-70">
{icon}
</div>
</div>
)
}
Example #4
Source File: NativeList.tsx From GTAV-NativeDB with MIT License | 5 votes |
export default function NativeList() {
const [filter, setFilter] = useState('')
const namespaces = useNativeSearch(filter)
const inputRef = useRef<HTMLInputElement>(null)
const history = useHistory()
const query = useQuery()
useEffect(() => {
const search = query.get('search')
setFilter(search ?? '')
}, [query, setFilter])
const handleSearchKeyDown = useCallback((e: KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
inputRef.current?.blur()
e.preventDefault()
}
}, [inputRef])
const handleSearchBlur = useCallback(() => {
if (filter) {
history.replace(`${history.location.pathname}?search=${encodeURIComponent(filter)}`)
}
else {
history.replace(history.location.pathname)
}
}, [history, filter])
const handleFilterChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setFilter(e.target.value)
}, [setFilter])
useSetAppBarSettings('NativeList', {
search: {
onChange: handleFilterChange,
onKeyDown: handleSearchKeyDown,
onBlur: handleSearchBlur,
ref: inputRef,
value: filter
}
})
useHotkeys('ctrl+k', () => {
inputRef.current?.focus()
}, {
filter: (event: globalThis.KeyboardEvent) => {
event.preventDefault()
return true
},
}, [inputRef])
return (
<Fragment>
<NativeListComponent
sx={{ height: '100%' }}
namespaces={namespaces}
/>
</Fragment>
)
}
Example #5
Source File: DownloadButton.tsx From prompts-ai with MIT License | 5 votes |
export default function DownloadButton(props: Props) { const prompt = useSelector(selectPrompt); const temperature = useSelector(selectTemperature); const topP = useSelector(selectTopP); const frequencyPenalty = useSelector(selectFrequencyPenalty); const presencePenalty = useSelector(selectPresencePenalty); const maxTokens = useSelector(selectMaxTokens); const stopSymbols = useSelector(selectStopSymbols); const modelName = useSelector(selectModelName); useHotkeys('ctrl+s,cmd+s', (event) => { event.preventDefault(); handleSaveAndDownload(); }); const handleSaveAndDownload = () => { const element = document.createElement("a"); const savedStopSymbols = stopSymbols.map(symbol => { return symbol.split('\\n').join('\n'); }); const file = new Blob([ JSON.stringify({prompt, temperature, topP, frequencyPenalty, presencePenalty, maxTokens, stopSymbols: savedStopSymbols, modelName, }) ], {type: 'text/plain'}); element.href = URL.createObjectURL(file); element.download = `gpt3_workspace_${Math.trunc(Date.now() / 1000)}.json`; document.body.appendChild(element); element.click(); } return <Button variant="outlined" color="default" className={props.className} size={'small'} startIcon={<SaveIcon />} onClick={handleSaveAndDownload} > Download </Button>; }
Example #6
Source File: App.tsx From prompts-ai with MIT License | 5 votes |
function App() {
const dispatch = useDispatch();
const theme = createMuiTheme({
palette: {
type: "dark"
}
});
useEffect(() => {
dispatch(normalizeConversations());
});
useHotkeys('ctrl+enter,cmd+enter', () => {
dispatch(fetchForCurrentTab());
}, {filter: () => true});
useHotkeys('ctrl+1', () => {
dispatch(updateTabIndex(0));
});
useHotkeys('ctrl+2', () => {
dispatch(updateTabIndex(1));
});
useHotkeys('ctrl+3', () => {
dispatch(updateTabIndex(2));
});
useHotkeys('ctrl+4', () => {
dispatch(updateTabIndex(3));
});
return (
<ThemeProvider theme={theme}>
<CssBaseline/>
<ApiKeyDialog/>
<TemplateDialog/>
<Header/>
<Container maxWidth={"lg"}>
<Box mt={2}>
<PromptEditor/>
</Box>
{/*<Box mt={2}>
<ModeTabs/>
</Box>*/}
<Box mt={2}>
<Typography>
Not affiliated with OpenAI. Feedback: [email protected].</Typography>
</Box>
</Container>
</ThemeProvider>
);
}
Example #7
Source File: useBoardEditorHotkeys.ts From datart with Apache License 2.0 | 5 votes |
export default function useBoardEditorHotkeys() {
const { undo, redo } = useContext(BoardActionContext);
const {
onEditDeleteActiveWidgets,
onEditLayerToTop,
onEditLayerToBottom,
onEditCopyWidgets,
onEditPasteWidgets,
} = useContext(WidgetActionContext);
useHotkeys('delete,backspace', () => onEditDeleteActiveWidgets(), []);
useHotkeys('ctrl+z,command+z', () => undo());
useHotkeys('ctrl+shift+z,command+shift+z', () => redo());
useHotkeys('ctrl+shift+up,command+shift+up', () => onEditLayerToTop());
useHotkeys('ctrl+shift+down,command+shift+down', () => onEditLayerToBottom());
useHotkeys('ctrl+c,command+c', () => onEditCopyWidgets());
useHotkeys('ctrl+v,command+v', () => onEditPasteWidgets());
//
useHotkeys('up', () => {
console.log('__ widgets up1');
});
useHotkeys('shift+up', () => {
console.log('__ widgets up10');
});
//
useHotkeys('down', () => {
console.log('__ widgets down1');
});
useHotkeys('shift+down', () => {
console.log('__ widgets down10');
});
//
useHotkeys('left', () => {
console.log('__ widgets left1');
});
useHotkeys('shift+left', () => {
console.log('__ widgets left10');
});
//
useHotkeys('right', () => {
console.log('__ widgets right1');
});
useHotkeys('shift+right', () => {
console.log('__ widgets right10');
});
useHotkeys('', () => {
console.log('__ widgets lock');
});
useHotkeys('', () => {
console.log('__ widgets unlock');
});
}
Example #8
Source File: useShortcuts.ts From openchakra with MIT License | 5 votes |
useShortcuts = () => {
const dispatch = useDispatch()
const selected = useSelector(getSelectedComponent)
const deleteNode = (event: KeyboardEvent | undefined) => {
if (event) {
event.preventDefault()
}
dispatch.components.deleteComponent(selected.id)
}
const toggleBuilderMode = (event: KeyboardEvent | undefined) => {
if (event && hasNoSpecialKeyPressed(event)) {
event.preventDefault()
dispatch.app.toggleBuilderMode()
}
}
const toggleCodePanel = (event: KeyboardEvent | undefined) => {
if (event && hasNoSpecialKeyPressed(event)) {
event.preventDefault()
dispatch.app.toggleCodePanel()
}
}
const undo = (event: KeyboardEvent | undefined) => {
if (event) {
event.preventDefault()
}
dispatch(UndoActionCreators.undo())
}
const redo = (event: KeyboardEvent | undefined) => {
if (event) {
event.preventDefault()
}
dispatch(UndoActionCreators.redo())
}
const onUnselect = () => {
dispatch.components.unselect()
}
const onSelectParent = (event: KeyboardEvent | undefined) => {
if (event && hasNoSpecialKeyPressed(event)) {
event.preventDefault()
dispatch.components.selectParent()
}
}
const onDuplicate = (event: KeyboardEvent | undefined) => {
if (event) {
event.preventDefault()
}
dispatch.components.duplicate()
}
const onKonamiCode = () => {
dispatch.components.loadDemo('secretchakra')
}
useHotkeys(keyMap.DELETE_NODE, deleteNode, {}, [selected.id])
useHotkeys(keyMap.TOGGLE_BUILDER_MODE, toggleBuilderMode)
useHotkeys(keyMap.TOGGLE_CODE_PANEL, toggleCodePanel)
useHotkeys(keyMap.UNDO, undo)
useHotkeys(keyMap.REDO, redo)
useHotkeys(keyMap.UNSELECT, onUnselect)
useHotkeys(keyMap.PARENT, onSelectParent)
useHotkeys(keyMap.DUPLICATE, onDuplicate)
useHotkeys(keyMap.KONAMI_CODE, onKonamiCode)
}
Example #9
Source File: SearchInput.tsx From phosphor-home with MIT License | 5 votes |
SearchInput: React.FC<SearchInputProps> = () => {
const [value, setValue] = useState<string>("");
const [query, setQuery] = useRecoilState(searchQueryAtom);
const inputRef = useRef<HTMLInputElement>() as MutableRefObject<HTMLInputElement>;
useHotkeys("ctrl+k,cmd+k", (e) => {
e.preventDefault();
if (!e.repeat) {
inputRef.current?.focus();
inputRef.current.select();
}
});
/* eslint-disable react-hooks/exhaustive-deps */
useEffect(() => {
let isMounted = true;
if (value !== query) {
isMounted && setValue(query);
ReactGA.event({ category: "Search", action: "Tag", label: query });
}
return () => void (isMounted = false);
}, [query]);
/* eslint-enable react-hooks/exhaustive-deps */
const [isReady] = useDebounce(
() => {
if (value !== query) {
setQuery(value);
!!value &&
ReactGA.event({ category: "Search", action: "Query", label: value });
}
!!value &&
void document
.getElementById("beacon")
?.scrollIntoView({ block: "start", behavior: "smooth" });
},
500,
[value]
);
const handleCancelSearch = () => {
setValue("");
// Should cancel pending debounce timeouts and immediately clear query
// without causing lag!
// setQuery("");
};
return (
<div className="search-bar">
<MagnifyingGlass id="search-icon" size={24} />
<input
ref={inputRef}
id="search-input"
title="Search for icon names, categories, or keywords"
aria-label="Search for an icon"
type="text"
autoCapitalize="off"
autoComplete="off"
value={value}
placeholder="Search"
onChange={({ currentTarget }) => setValue(currentTarget.value)}
onKeyPress={({ currentTarget, key }) =>
key === "Enter" && currentTarget.blur()
}
/>
{!value && !isMobile && <Keys>{isApple ? <Command /> : "Ctrl + "}K</Keys>}
{value ? (
isReady() ? (
<X className="clear-icon" size={18} onClick={handleCancelSearch} />
) : (
<HourglassHigh className="wait-icon" weight="fill" size={18} />
)
) : null}
</div>
);
}
Example #10
Source File: FeatureFlag.tsx From condo with MIT License | 5 votes |
FeatureFlagsController: React.FC = () => {
useHotkeys('d+o+m+a', () => setIsModalVisible(true))
const intl = useIntl()
const featureFlagsTitle = intl.formatMessage({ id: 'FeatureFlags.Modal.Title' } )
const featureFlagsDescription = intl.formatMessage({ id: 'FeatureFlags.Modal.Description' } )
const [isModalVisible, setIsModalVisible] = useState(false)
const allFeatures = getAllFeatures()
const handleOk = () => {
setIsModalVisible(false)
window.location.reload()
}
const enabledFlags = getEnabledFeatures()
return (
<>
<Modal title={ featureFlagsTitle }
visible={isModalVisible}
onCancel={() => setIsModalVisible(false)}
onOk={handleOk}
cancelButtonProps={{ disabled: true }}
>
<Alert message={ featureFlagsDescription } type="success" />
{
allFeatures.map((name) => (
<>
<div style={{ paddingTop: '30px' }}>
<h2><b>{name}</b></h2>
<Switch defaultChecked={enabledFlags.includes(name)} checkedChildren="1" unCheckedChildren="0" onChange={() => toggleFeature(name)} />
</div>
</>
))
}
</Modal>
</>
)
}
Example #11
Source File: ArrowButtons.tsx From frontend with Apache License 2.0 | 5 votes |
ArrowButtons: React.FunctionComponent<{
testRuns: TestRun[];
selectedTestRunIndex: number;
handleNavigation: (testRunId: string) => void;
}> = ({ testRuns, selectedTestRunIndex, handleNavigation }) => {
const classes = useStyles();
const navigateNext = () => {
if (selectedTestRunIndex + 1 < testRuns.length) {
const next = testRuns[selectedTestRunIndex + 1];
handleNavigation(next.id);
}
};
const navigateBefore = () => {
if (selectedTestRunIndex > 0) {
const prev = testRuns[selectedTestRunIndex - 1];
handleNavigation(prev.id);
}
};
useHotkeys("right", navigateNext, [selectedTestRunIndex, handleNavigation]);
useHotkeys("left", navigateBefore, [selectedTestRunIndex, handleNavigation]);
return (
<React.Fragment>
{selectedTestRunIndex + 1 < testRuns.length && (
<Tooltip title={"Hotkey: ArrowRight"}>
<IconButton
color="secondary"
className={classes.button}
style={{
right: 0,
}}
onClick={navigateNext}
>
<NavigateNext className={classes.icon} />
</IconButton>
</Tooltip>
)}
{selectedTestRunIndex > 0 && (
<Tooltip title={"Hotkey: ArrowLeft"}>
<IconButton
color="secondary"
className={classes.button}
style={{
left: 0,
}}
onClick={navigateBefore}
>
<NavigateBefore className={classes.icon} />
</IconButton>
</Tooltip>
)}
</React.Fragment>
);
}
Example #12
Source File: ApproveRejectButtons.tsx From frontend with Apache License 2.0 | 5 votes |
ApproveRejectButtons: React.FunctionComponent<{
testRun: TestRun;
}> = ({ testRun }) => {
const { enqueueSnackbar } = useSnackbar();
const approve = () => {
testRunService
.approveBulk([testRun.id], testRun.merge)
.then(() =>
enqueueSnackbar("Approved", {
variant: "success",
})
)
.catch((err) =>
enqueueSnackbar(err, {
variant: "error",
})
);
};
const reject = () => {
testRunService
.rejectBulk([testRun.id])
.then(() =>
enqueueSnackbar("Rejected", {
variant: "success",
})
)
.catch((err) =>
enqueueSnackbar(err, {
variant: "error",
})
);
};
useHotkeys("a", approve, [testRun]);
useHotkeys("x", reject, [testRun]);
return (
<Grid container spacing={2} alignItems="center">
{testRun.merge && (
<Grid item>
<Tooltip title="Will replace target branch baseline if accepted">
<Chip
label={`merge into: ${testRun.baselineBranchName}`}
color="secondary"
size="small"
/>
</Tooltip>
</Grid>
)}
<Grid item>
<Tooltip title={"Hotkey: A"}>
<Button color="inherit" onClick={approve}>
Approve
</Button>
</Tooltip>
</Grid>
<Grid item>
<Tooltip title={"Hotkey: X"}>
<Button color="secondary" onClick={reject}>
Reject
</Button>
</Tooltip>
</Grid>
</Grid>
);
}
Example #13
Source File: header.tsx From react-notion-x with MIT License | 5 votes |
Search: React.FC<{
block: types.Block
search?: SearchNotionFn
title?: React.ReactNode
}> = ({ block, search, title = 'Search' }) => {
const { searchNotion, rootPageId } = useNotionContext()
const onSearchNotion = search || searchNotion
const [isSearchOpen, setIsSearchOpen] = React.useState(false)
const onOpenSearch = React.useCallback(() => {
setIsSearchOpen(true)
}, [])
const onCloseSearch = React.useCallback(() => {
setIsSearchOpen(false)
}, [])
useHotkeys('cmd+p', (event) => {
onOpenSearch()
event.preventDefault()
event.stopPropagation()
})
useHotkeys('cmd+k', (event) => {
onOpenSearch()
event.preventDefault()
event.stopPropagation()
})
const hasSearch = !!onSearchNotion
return (
<>
{hasSearch && (
<div
role='button'
className={cs('breadcrumb', 'button', 'notion-search-button')}
onClick={onOpenSearch}
>
<SearchIcon className='searchIcon' />
{title && <span className='title'>{title}</span>}
</div>
)}
{isSearchOpen && hasSearch && (
<SearchDialog
isOpen={isSearchOpen}
rootBlockId={rootPageId || block?.id}
onClose={onCloseSearch}
searchNotion={onSearchNotion}
/>
)}
</>
)
}
Example #14
Source File: search-bar.tsx From admin with MIT License | 5 votes |
SearchBar: React.FC = () => {
const [showSearchModal, setShowSearchModal] = useState(false)
const toggleSearch = (e) => {
e.preventDefault()
e.stopPropagation()
setShowSearchModal((show) => !show)
}
const closeModal = () => {
setShowSearchModal(false)
}
useHotkeys("cmd+k", toggleSearch, {}, [])
useHotkeys("ctrl+k", toggleSearch, {}, [])
useHotkeys("/", toggleSearch, {}, [])
React.useEffect(() => {
return globalHistory.listen(({ action }) => {
if (action === "PUSH") {
closeModal()
}
})
}, [])
return (
<>
<button
onClick={() => setShowSearchModal(true)}
className="flex basis-1/2 items-center px-small py-[6px]"
>
<SearchIcon className="text-grey-40" />
<div className="ml-5">
<OSShortcut macModifiers="⌘" winModifiers="Ctrl" keys="K" />
</div>
<span className="ml-xsmall text-grey-40 inter-base-regular">
Search anything...
</span>
</button>
{showSearchModal && <SearchModal handleClose={closeModal} />}
</>
)
}
Example #15
Source File: StudentQueue.tsx From office-hours with GNU General Public License v3.0 | 4 votes |
export default function StudentQueue({
qid,
cid,
}: StudentQueueProps): ReactElement {
const { queue } = useQueue(qid);
const { questions, mutateQuestions } = useQuestions(qid);
const { studentQuestion, studentQuestionIndex } = useStudentQuestion(qid);
const [isFirstQuestion, setIsFirstQuestion] = useLocalStorage(
"isFirstQuestion",
true
);
const [showJoinPopconfirm, setShowJoinPopconfirm] = useState(false);
const { deleteDraftQuestion } = useDraftQuestion();
const [isJoining, setIsJoining] = useState(
questions &&
studentQuestion &&
studentQuestion?.status !== OpenQuestionStatus.Queued
);
const [popupEditQuestion, setPopupEditQuestion] = useState(false);
const router = useRouter();
const editQuestionQueryParam = Boolean(router.query.edit_question as string);
useEffect(() => {
if (editQuestionQueryParam && studentQuestion) {
mutate(`/api/v1/queues/${qid}/questions`);
setPopupEditQuestion(true);
router.push(`/course/${cid}/queue/${qid}`);
}
}, [editQuestionQueryParam, qid, studentQuestion]);
const studentQuestionId = studentQuestion?.id;
const studentQuestionStatus = studentQuestion?.status;
const leaveQueue = useCallback(async () => {
await API.questions.update(studentQuestionId, {
status: ClosedQuestionStatus.ConfirmedDeleted,
});
setIsJoining(false);
await mutateQuestions();
}, [mutateQuestions, studentQuestionId]);
const rejoinQueue = useCallback(async () => {
await API.questions.update(studentQuestionId, {
status: OpenQuestionStatus.PriorityQueued,
});
await mutateQuestions();
}, [mutateQuestions, studentQuestionId]);
const finishQuestion = useCallback(
async (text: string, questionType: QuestionType, groupable: boolean) => {
const updateStudent = {
text,
questionType,
groupable,
status:
studentQuestionStatus === OpenQuestionStatus.Drafting
? OpenQuestionStatus.Queued
: studentQuestionStatus,
};
const updatedQuestionFromStudent = await API.questions.update(
studentQuestionId,
updateStudent
);
const newQuestionsInQueue = questions?.queue?.map((question: Question) =>
question.id === studentQuestionId
? updatedQuestionFromStudent
: question
);
// questions are the old questions and newQuestionsInQueue are questions that've been added since.
mutateQuestions({
...questions,
yourQuestion: updatedQuestionFromStudent,
queue: newQuestionsInQueue,
});
},
[studentQuestionStatus, studentQuestionId, questions, mutateQuestions]
);
const joinQueueAfterDeletion = useCallback(async () => {
await API.questions.update(studentQuestion?.id, {
status: ClosedQuestionStatus.ConfirmedDeleted,
});
await mutateQuestions();
const newQuestion = await API.questions.create({
text: studentQuestion.text,
questionType: studentQuestion?.questionType,
queueId: qid,
location: studentQuestion?.location,
force: true,
groupable: false,
});
await API.questions.update(newQuestion.id, {
status: OpenQuestionStatus.Queued,
});
await mutateQuestions();
}, [mutateQuestions, qid, studentQuestion]);
const openEditModal = useCallback(async () => {
mutate(`/api/v1/queues/${qid}/questions`);
setPopupEditQuestion(true);
}, [qid]);
const closeEditModal = useCallback(() => {
setPopupEditQuestion(false);
setIsJoining(false);
}, []);
const leaveQueueAndClose = useCallback(() => {
//delete draft when they leave the queue
deleteDraftQuestion();
leaveQueue();
closeEditModal();
}, [deleteDraftQuestion, leaveQueue, closeEditModal]);
const joinQueueOpenModal = useCallback(
async (force: boolean) => {
try {
const createdQuestion = await API.questions.create({
queueId: Number(qid),
text: "",
force: force,
questionType: null,
groupable: false,
});
const newQuestionsInQueue = [...questions?.queue, createdQuestion];
await mutateQuestions({ ...questions, queue: newQuestionsInQueue });
setPopupEditQuestion(true);
return true;
} catch (e) {
if (
e.response?.data?.message?.includes(
ERROR_MESSAGES.questionController.createQuestion.oneQuestionAtATime
)
) {
return false;
}
return true;
// TODO: how should we handle error that happens for another reason?
}
},
[mutateQuestions, qid, questions]
);
const finishQuestionAndClose = useCallback(
(
text: string,
qt: QuestionType,
groupable: true,
router: Router,
cid: number
) => {
deleteDraftQuestion();
finishQuestion(text, qt, groupable);
closeEditModal();
if (isFirstQuestion) {
notification.warn({
style: { cursor: "pointer" },
message: "Enable Notifications",
className: "hide-in-percy",
description:
"Turn on notifications for when it's almost your turn to get help.",
placement: "bottomRight",
duration: 0,
onClick: () => {
notification.destroy();
setIsFirstQuestion(false);
router.push(`/settings?cid=${cid}`);
},
});
}
},
[
deleteDraftQuestion,
finishQuestion,
closeEditModal,
isFirstQuestion,
setIsFirstQuestion,
]
);
useHotkeys(
"shift+e",
() => {
if (studentQuestion) {
openEditModal();
}
},
[studentQuestion]
);
useHotkeys(
"shift+n",
() => {
if (!studentQuestion && queue?.allowQuestions && !queue?.isDisabled) {
joinQueueOpenModal(false).then((res) => setShowJoinPopconfirm(!res));
}
},
[studentQuestion, queue]
);
if (queue && questions) {
if (!queue.isOpen) {
return <h1 style={{ marginTop: "50px" }}>The Queue is Closed!</h1>;
}
return (
<>
<Container>
<CantFindModal
visible={studentQuestion?.status === LimboQuestionStatus.CantFind}
leaveQueue={leaveQueue}
rejoinQueue={rejoinQueue}
/>
<StudentRemovedFromQueueModal
question={studentQuestion}
leaveQueue={leaveQueue}
joinQueue={joinQueueAfterDeletion}
/>
<QueueInfoColumn
queueId={qid}
isStaff={false}
buttons={
!studentQuestion && (
<Popconfirm
title={
<PopConfirmTitle>
You already have a question in a queue for this course, so
your previous question will be deleted in order to join
this queue. Do you want to continue?
</PopConfirmTitle>
}
onConfirm={() => joinQueueOpenModal(true)}
okText="Yes"
cancelText="No"
disabled
visible={showJoinPopconfirm}
onVisibleChange={setShowJoinPopconfirm}
>
<JoinButton
type="primary"
disabled={!queue?.allowQuestions || queue?.isDisabled}
data-cy="join-queue-button"
onClick={async () =>
setShowJoinPopconfirm(!(await joinQueueOpenModal(false)))
}
>
Join Queue
</JoinButton>
</Popconfirm>
)
}
/>
<VerticalDivider />
<QueueListContainer>
{studentQuestion && (
<>
<StudentBanner
queueId={qid}
editQuestion={openEditModal}
leaveQueue={leaveQueue}
/>
<div style={{ marginTop: "40px" }} />
</>
)}
<QueueQuestions
questions={questions?.queue}
studentQuestion={studentQuestion}
/>
</QueueListContainer>
</Container>
<QuestionForm
visible={
(questions && !studentQuestion && isJoining) ||
// && studentQuestion.status !== QuestionStatusKeys.Drafting)
popupEditQuestion
}
question={studentQuestion}
leaveQueue={leaveQueueAndClose}
finishQuestion={finishQuestionAndClose}
position={studentQuestionIndex + 1}
cancel={closeEditModal}
/>
</>
);
} else {
return <div />;
}
}
Example #16
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 #17
Source File: QueryYakScriptParam.tsx From yakit with GNU Affero General Public License v3.0 | 4 votes |
QueryYakScriptParamSelector: React.FC<QueryYakScriptParamProp> = React.memo((props) => {
const {loading, allTag, onAllTag, isAll, onIsAll, historyTask} = props
// 下拉框选中tag值
const selectRef = useRef(null)
const [itemSelects, setItemSelects] = useState<string[]>([])
const [selectLoading, setSelectLoading] = useState<boolean>(true)
useEffect(() => {
setTimeout(() => setSelectLoading(false), 300)
}, [selectLoading])
// 设置本地搜索 tags 的状态
const [searchTag, setSearchTag] = useState("");
// 用于存储 tag 的搜索与结果
const [topTags, setTopTags] = useState<FieldName[]>([]);
const [topN, setTopN] = useState(15);
const [selectedTags, setSelectedTags] = useState<string[]>([]);
// 更新搜索,这个也可以用于后端保存
const [params, setParams] = useState<SimpleQueryYakScriptSchema>(props.params)
useEffect(() => {
props.onParams(params)
}, [params])
// 辅助变量
const [updateTagsSelectorTrigger, setUpdateTagsSelector] = useState(false);
// 设置最大最小值
const [minTagWeight, setMinTagWeight] = useState(1);
const [maxTagWeight, setMaxTagWeight] = useState(2000);
useEffect(() => {
let count = 0;
const showTags = allTag.filter(d => {
if (
count <= topN // 限制数量
&& d.Total >= minTagWeight && d.Total <= maxTagWeight
&& !selectedTags.includes(d.Name)
&& d.Name.toLowerCase().includes(searchTag.toLowerCase()) // 设置搜索结果
) {
count++
return true
}
return false
})
setTopTags([...showTags])
}, [
allTag,
useDebounce(minTagWeight, {wait: 500}),
useDebounce(maxTagWeight, {wait: 500}),
useDebounce(searchTag, {wait: 500}),
useDebounce(selectedTags, {wait: 500}),
useDebounce(topN, {wait: 500}),
updateTagsSelectorTrigger,
])
const updateTagsSelector = () => {
setUpdateTagsSelector(!updateTagsSelectorTrigger)
}
const syncTags = useMemoizedFn(() => {
setParams({
type: params.type,
tags: selectedTags.join(","),
include: params.include,
exclude: [],
})
})
useEffect(() => {
const tags = historyTask ? historyTask.split(",") : []
setSelectedTags(tags)
}, [historyTask])
// 更新 params Tags
useEffect(() => {
syncTags()
}, [useDebounce(selectedTags, {wait: 300})])
useEffect(() => {
setTopN(10)
}, [searchTag])
const selectedAll = useMemoizedFn(() => {
if (!selectRef || !selectRef.current) return
const ref = selectRef.current as unknown as HTMLDivElement
ref.blur()
setTimeout(() => {
onIsAll(true)
setItemSelects([])
setSearchTag("")
setParams({type: params.type, tags: "", include: [], exclude: []})
}, 200);
})
const selectDropdown = useMemoizedFn((originNode: React.ReactNode) => {
return (
<div>
<Spin spinning={selectLoading}>
<div className="select-render-all" onClick={selectedAll}>全选</div>
{originNode}
</Spin>
</div>
)
})
const saveConfigTemplate = useMemoizedFn(() => {
let m = showModal({
title: "导出批量扫描配置",
width: "50%",
content: (
<>
<SaveConfig QueryConfig={params} onSave={filename => {
info(`保存到 ${filename}`)
m.destroy()
}}/>
</>
),
})
})
useHotkeys("alt+p", saveConfigTemplate)
const importConfigTemplate = useMemoizedFn(() => {
let m = showModal({
title: "导出批量扫描配置",
width: "50%",
content: (
<>
<ImportConfig/>
</>
),
})
})
useHotkeys("alt+u", importConfigTemplate)
return (
<AutoCard
size={"small"}
bordered={true}
title={"选择插件"}
extra={
<Space>
<Popconfirm
title={"强制更新讲重新构建 Tags 索引"}
onConfirm={() => onAllTag()}
>
<a href={"#"}>强制更新 Tags</a>
</Popconfirm>
<Popconfirm
title={"清空已选 Tag?"}
onConfirm={() => {
onIsAll(false)
setSelectedTags([])
setParams({type: params.type, tags: "", include: [], exclude: []})
updateTagsSelector()
}}
>
<Button size={"small"} type={"link"} danger={true}>
清空
</Button>
</Popconfirm>
</Space>
}
loading={loading}
bodyStyle={{display: "flex", flexDirection: "column", overflow: "hidden"}}
>
<div className="div-width-100" style={{maxHeight: 237}}>
<div className="div-width-100">
<Form size="small">
<ItemSelects
item={{
style: {marginBottom: 0},
label: "设置Tag"
}}
select={{
ref: selectRef,
className: "div-width-100",
allowClear: true,
autoClearSearchValue: false,
maxTagCount: "responsive",
mode: "multiple",
data: topTags,
optValue: "Name",
optionLabelProp: "Name",
renderOpt: (info: FieldName) => {
return <div style={{display: "flex", justifyContent: "space-between"}}>
<span>{info.Name}</span><span>{info.Total}</span></div>
},
value: itemSelects,
onSearch: (keyword: string) => setSearchTag(keyword),
setValue: (value) => setItemSelects(value),
onDropdownVisibleChange: (open) => {
if (open) {
setItemSelects([])
setSearchTag("")
} else {
const filters = itemSelects.filter(item => !selectedTags.includes(item))
setSelectedTags(selectedTags.concat(filters))
setItemSelects([])
setSearchTag("")
}
},
onPopupScroll: (e) => {
const {target} = e
const ref: HTMLDivElement = target as unknown as HTMLDivElement
if (ref.scrollTop + ref.offsetHeight === ref.scrollHeight) {
setSelectLoading(true)
setTopN(topN + 10)
}
},
dropdownRender: (originNode: React.ReactNode) => selectDropdown(originNode)
}}
></ItemSelects>
</Form>
</div>
<Divider style={{margin: "6px 0"}}/>
{(isAll || selectedTags.length !== 0) && (
<div className='div-width-100 div-height-100' style={{maxHeight: 200}}>
<AutoCard
size='small'
bordered={false}
bodyStyle={{overflow: "hidden auto", padding: 2}}
>
{isAll ? (
<Tag
style={{marginBottom: 2}}
color={"blue"}
onClose={() => {
onIsAll(false)
setSelectedTags([])
setParams({type: params.type, tags: "", include: [], exclude: []})
updateTagsSelector()
}}
closable={true}
>
全选
</Tag>
) : (
selectedTags.map((i) => {
return (
<Tag
key={i}
style={{marginBottom: 2}}
color={"blue"}
onClose={() => {
setSelectedTags(selectedTags.filter((element) => i !== element))
}}
closable={true}
>
{i}
</Tag>
)
})
)}
</AutoCard>
</div>
)}
</div>
<div style={{flex: 1, overflow: "hidden", paddingTop: 0, marginTop: 2}}>
<SearchYakScriptForFilter
simpleFilter={params}
isAll={isAll}
onInclude={(i) => {
setParams({
...params,
include: isAll ? [...params.include] : [...params.include, i.ScriptName],
exclude: [...params.exclude.filter((target) => i.ScriptName != target)]
})
}}
onExclude={(i) => {
const existedInTag = isAll ? true :
params.tags
.split(",")
.filter((tag) => !!tag)
.filter((tag) => i.Tags.includes(tag)).length > 0
if (existedInTag) {
setParams({
...params,
exclude: [...params.exclude, i.ScriptName],
include: [...params.include.filter((target) => i.ScriptName != target)]
})
} else {
setParams({
...params,
include: [...params.include.filter((target) => i.ScriptName != target)]
})
}
}}
/>
</div>
</AutoCard>
)
})
Example #18
Source File: MainOperator.tsx From yakit with GNU Affero General Public License v3.0 | 4 votes |
Main: React.FC<MainProp> = (props) => {
const [engineStatus, setEngineStatus] = useState<"ok" | "error">("ok")
const [status, setStatus] = useState<{ addr: string; isTLS: boolean }>()
const [collapsed, setCollapsed] = useState(false)
const [hideMenu, setHideMenu] = useState(false)
const [loading, setLoading] = useState(false)
const [menuItems, setMenuItems] = useState<MenuItemGroup[]>([])
const [routeMenuData, setRouteMenuData] = useState<MenuDataProps[]>(RouteMenuData)
const [notification, setNotification] = useState("")
const [pageCache, setPageCache] = useState<PageCache[]>([
{
verbose: "MITM",
route: Route.HTTPHacker,
singleNode: ContentByRoute(Route.HTTPHacker),
multipleNode: []
}
])
const [currentTabKey, setCurrentTabKey] = useState<string>(Route.HTTPHacker)
// 系统类型
const [system, setSystem] = useState<string>("")
useEffect(() => {
ipcRenderer.invoke('fetch-system-name').then((res) => setSystem(res))
}, [])
// yakit页面关闭是否二次确认提示
const [winCloseFlag, setWinCloseFlag] = useState<boolean>(true)
const [winCloseShow, setWinCloseShow] = useState<boolean>(false)
useEffect(() => {
ipcRenderer
.invoke("get-value", WindowsCloseFlag)
.then((flag: any) => setWinCloseFlag(flag === undefined ? true : flag))
}, [])
// 获取自定义菜单
const updateMenuItems = () => {
setLoading(true)
// Fetch User Defined Plugins
ipcRenderer
.invoke("GetAllMenuItem", {})
.then((data: { Groups: MenuItemGroup[] }) => {
setMenuItems(data.Groups)
})
.catch((e: any) => failed("Update Menu Item Failed"))
.finally(() => setTimeout(() => setLoading(false), 300))
// Fetch Official General Plugins
ipcRenderer
.invoke("QueryYakScript", {
Pagination: genDefaultPagination(1000),
IsGeneralModule: true,
Type: "yak"
} as QueryYakScriptRequest)
.then((data: QueryYakScriptsResponse) => {
const tabList: MenuDataProps[] = cloneDeep(RouteMenuData)
for (let item of tabList) {
if (item.subMenuData) {
if (item.key === Route.GeneralModule) {
const extraMenus: MenuDataProps[] = data.Data.map((i) => {
return {
icon: <EllipsisOutlined/>,
key: `plugin:${i.Id}`,
label: i.ScriptName,
} as unknown as MenuDataProps
})
item.subMenuData.push(...extraMenus)
}
item.subMenuData.sort((a, b) => a.label.localeCompare(b.label))
}
}
setRouteMenuData(tabList)
})
}
useEffect(() => {
updateMenuItems()
ipcRenderer.on("fetch-new-main-menu", (e) => {
updateMenuItems()
})
return () => {
ipcRenderer.removeAllListeners("fetch-new-main-menu")
}
}, [])
useEffect(() => {
if (engineStatus === "error") props.onErrorConfirmed && props.onErrorConfirmed()
}, [engineStatus])
// 整合路由对应名称
const pluginKey = (item: PluginMenuItem) => `plugin:${item.Group}:${item.YakScriptId}`;
const routeKeyToLabel = new Map<string, string>();
routeMenuData.forEach(k => {
(k.subMenuData || []).forEach(subKey => {
routeKeyToLabel.set(`${subKey.key}`, subKey.label)
})
routeKeyToLabel.set(`${k.key}`, k.label)
})
menuItems.forEach((k) => {
k.Items.forEach((value) => {
routeKeyToLabel.set(pluginKey(value), value.Verbose)
})
})
// Tabs Bar Operation Function
const getCacheIndex = (route: string) => {
const targets = pageCache.filter((i) => i.route === route)
return targets.length > 0 ? pageCache.indexOf(targets[0]) : -1
}
const addTabPage = useMemoizedFn(
(route: Route, nodeParams?: { time?: string; node: ReactNode; isRecord?: boolean }) => {
const filterPage = pageCache.filter((i) => i.route === route)
const filterPageLength = filterPage.length
if (singletonRoute.includes(route)) {
if (filterPageLength > 0) {
setCurrentTabKey(route)
} else {
const tabName = routeKeyToLabel.get(route) || `${route}`
setPageCache([
...pageCache,
{
verbose: tabName,
route: route,
singleNode: ContentByRoute(route),
multipleNode: []
}
])
setCurrentTabKey(route)
}
} else {
if (filterPageLength > 0) {
const tabName = routeKeyToLabel.get(route) || `${route}`
const tabId = `${route}-[${randomString(49)}]`
const time = new Date().getTime().toString()
const node: multipleNodeInfo = {
id: tabId,
verbose: `${tabName}-[${filterPage[0].multipleNode.length + 1}]`,
node: nodeParams && nodeParams.node ? nodeParams?.node || <></> : ContentByRoute(route),
time: nodeParams && nodeParams.node ? nodeParams?.time || time : time
}
const pages = pageCache.map((item) => {
if (item.route === route) {
item.multipleNode.push(node)
item.multipleCurrentKey = tabId
return item
}
return item
})
setPageCache([...pages])
setCurrentTabKey(route)
if (nodeParams && !!nodeParams.isRecord) addFuzzerList(nodeParams?.time || time)
} else {
const tabName = routeKeyToLabel.get(route) || `${route}`
const tabId = `${route}-[${randomString(49)}]`
const time = new Date().getTime().toString()
const node: multipleNodeInfo = {
id: tabId,
verbose: `${tabName}-[1]`,
node: nodeParams && nodeParams.node ? nodeParams?.node || <></> : ContentByRoute(route),
time: nodeParams && nodeParams.node ? nodeParams?.time || time : time
}
setPageCache([
...pageCache,
{
verbose: tabName,
route: route,
singleNode: undefined,
multipleNode: [node],
multipleCurrentKey: tabId
}
])
setCurrentTabKey(route)
if (nodeParams && !!nodeParams.isRecord) addFuzzerList(nodeParams?.time || time)
}
}
}
)
const menuAddPage = useMemoizedFn((route: Route) => {
if (route === "ignore") return
if (route === Route.HTTPFuzzer) {
const time = new Date().getTime().toString()
addTabPage(Route.HTTPFuzzer, {
time: time,
node: ContentByRoute(Route.HTTPFuzzer, undefined, {
system: system,
order: time
}),
isRecord: true
})
} else addTabPage(route as Route)
})
const removePage = (route: string) => {
const targetIndex = getCacheIndex(route)
if (targetIndex > 0 && pageCache[targetIndex - 1]) {
const targetCache = pageCache[targetIndex - 1]
setCurrentTabKey(targetCache.route)
}
if (targetIndex === 0 && pageCache[targetIndex + 1]) {
const targetCache = pageCache[targetIndex + 1]
setCurrentTabKey(targetCache.route)
}
if (targetIndex === 0 && pageCache.length === 1) setCurrentTabKey("")
setPageCache(pageCache.filter((i) => i.route !== route))
if (route === Route.HTTPFuzzer) delFuzzerList(1)
}
const updateCacheVerbose = (id: string, verbose: string) => {
const index = getCacheIndex(id)
if (index < 0) return
pageCache[index].verbose = verbose
setPageCache([...pageCache])
}
const setMultipleCurrentKey = useMemoizedFn((key: string, type: Route) => {
const arr = pageCache.map(item => {
if (item.route === type) {
item.multipleCurrentKey = key
return item
}
return item
})
setPageCache([...arr])
})
const removeMultipleNodePage = useMemoizedFn((key: string, type: Route) => {
const removeArr: multipleNodeInfo[] = pageCache.filter(item => item.route === type)[0]?.multipleNode || []
if (removeArr.length === 0) return
const nodes = removeArr.filter(item => item.id === key)
const time = nodes[0].time
let index = 0
for (let i in removeArr) {
if (removeArr[i].id === key) {
index = +i
break
}
}
if (index === 0 && removeArr.length === 1) {
removePage(type)
return
}
let current = ""
let filterArr: multipleNodeInfo[] = []
if (index > 0 && removeArr[index - 1]) {
current = removeArr[index - 1].id
filterArr = removeArr.filter(item => item.id !== key)
}
if (index === 0 && removeArr[index + 1]) {
current = removeArr[index + 1].id
filterArr = removeArr.filter(item => item.id !== key)
}
if (current) {
const arr = pageCache.map(item => {
if (item.route === type) {
item.multipleNode = [...filterArr]
item.multipleCurrentKey = current
return item
}
return item
})
setPageCache([...arr])
if (type === Route.HTTPFuzzer) delFuzzerList(2, time)
}
})
const removeOtherMultipleNodePage = useMemoizedFn((key: string, type: Route) => {
const removeArr: multipleNodeInfo[] = pageCache.filter(item => item.route === type)[0]?.multipleNode || []
if (removeArr.length === 0) return
const nodes = removeArr.filter(item => item.id === key)
const time = nodes[0].time
const arr = pageCache.map(item => {
if (item.route === type) {
item.multipleNode = [...nodes]
item.multipleCurrentKey = key
return item
}
return item
})
setPageCache([...arr])
if (type === Route.HTTPFuzzer) delFuzzerList(3, time)
})
// 全局记录鼠标坐标位置(为右键菜单提供定位)
const coordinateTimer = useRef<any>(null)
useEffect(() => {
document.onmousemove = (e) => {
const {screenX, screenY, clientX, clientY, pageX, pageY} = e
if (coordinateTimer.current) {
clearTimeout(coordinateTimer.current)
coordinateTimer.current = null
}
coordinateTimer.current = setTimeout(() => {
coordinate.screenX = screenX
coordinate.screenY = screenY
coordinate.clientX = clientX
coordinate.clientY = clientY
coordinate.pageX = pageX
coordinate.pageY = pageY
}, 50);
}
}, [])
// 全局注册快捷键功能
const documentKeyDown = useMemoizedFn((e: any) => {
// ctrl + w 关闭tab页面
if (e.code === "KeyW" && (e.ctrlKey || e.metaKey)) {
e.preventDefault()
if (pageCache.length === 0) return
setLoading(true)
removePage(currentTabKey)
setTimeout(() => setLoading(false), 300);
return
}
})
useEffect(() => {
document.onkeydown = documentKeyDown
}, [])
// fuzzer本地缓存
const fuzzerList = useRef<Map<string, fuzzerInfoProp>>(new Map<string, fuzzerInfoProp>())
const saveFuzzerList = debounce(() => {
const historys: fuzzerInfoProp[] = []
fuzzerList.current.forEach((value) => historys.push(value))
historys.sort((a, b) => +a.time - +b.time)
const filters = historys.filter(item => (item.request || "").length < 1000000 && (item.request || "").length > 0)
ipcRenderer.invoke("set-value", FuzzerCache, JSON.stringify(filters.slice(-5)))
}, 500)
const fetchFuzzerList = useMemoizedFn(() => {
setLoading(true)
fuzzerList.current.clear()
ipcRenderer
.invoke("get-value", FuzzerCache)
.then((res: any) => {
const cache = JSON.parse(res || "[]")
for (let item of cache) {
const time = new Date().getTime().toString()
fuzzerList.current.set(time, {...item, time: time})
addTabPage(Route.HTTPFuzzer, {
time: time,
node:
ContentByRoute(
Route.HTTPFuzzer,
undefined,
{
isHttps: item.isHttps || false,
request: item.request || "",
fuzzerParams: item,
system: system,
order: time
}
)
})
}
})
.catch((e) => console.info(e))
.finally(() => setTimeout(() => setLoading(false), 300))
})
const addFuzzerList = (key: string, request?: string, isHttps?: boolean) => {
fuzzerList.current.set(key, {request, isHttps, time: key})
}
const delFuzzerList = (type: number, key?: string) => {
if (type === 1) fuzzerList.current.clear()
if (type === 2 && key) if (fuzzerList.current.has(key)) fuzzerList.current.delete(key)
if (type === 3 && key) {
const info = fuzzerList.current.get(key)
if (info) {
fuzzerList.current.clear()
fuzzerList.current.set(key, info)
}
}
saveFuzzerList()
}
const updateFuzzerList = (key: string, param: fuzzerInfoProp) => {
fuzzerList.current.set(key, param)
saveFuzzerList()
}
useEffect(() => {
ipcRenderer.on("fetch-fuzzer-setting-data", (e, res: any) => updateFuzzerList(res.key, JSON.parse(res.param)))
// 开发环境不展示fuzzer缓存
ipcRenderer.invoke("is-dev").then((flag) => {
if (!flag) fetchFuzzerList()
// fetchFuzzerList()
})
return () => ipcRenderer.removeAllListeners("fetch-fuzzer-setting-data")
}, [])
// 加载补全
useEffect(() => {
ipcRenderer.invoke("GetYakitCompletionRaw").then((data: { RawJson: Uint8Array }) => {
const completionJson = Buffer.from(data.RawJson).toString("utf8")
setCompletions(JSON.parse(completionJson) as CompletionTotal)
// success("加载 Yak 语言自动补全成功 / Load Yak IDE Auto Completion Finished")
})
}, [])
useEffect(() => {
ipcRenderer.invoke("yakit-connect-status").then((data) => {
setStatus(data)
})
ipcRenderer.on("client-engine-status-ok", (e, reason) => {
if (engineStatus !== "ok") setEngineStatus("ok")
})
ipcRenderer.on("client-engine-status-error", (e, reason) => {
if (engineStatus === "ok") setEngineStatus("error")
})
const updateEngineStatus = () => {
ipcRenderer
.invoke("engine-status")
.catch((e: any) => {
setEngineStatus("error")
})
.finally(() => {
})
}
let id = setInterval(updateEngineStatus, 3000)
return () => {
ipcRenderer.removeAllListeners("client-engine-status-error")
ipcRenderer.removeAllListeners("client-engine-status-ok")
clearInterval(id)
}
}, [])
useHotkeys("Ctrl+Alt+T", () => {
})
useEffect(() => {
ipcRenderer.invoke("query-latest-notification").then((e: string) => {
setNotification(e)
if (e) {
success(
<>
<Space direction={"vertical"}>
<span>来自于 yaklang.io 的通知</span>
<Button
type={"link"}
onClick={() => {
showModal({
title: "Notification",
content: (
<>
<MDEditor.Markdown source={e}/>
</>
)
})
}}
>
点击查看
</Button>
</Space>
</>
)
}
})
}, [])
// 新增数据对比页面
useEffect(() => {
ipcRenderer.on("main-container-add-compare", (e, params) => {
const newTabId = `${Route.DataCompare}-[${randomString(49)}]`;
const verboseNameRaw = routeKeyToLabel.get(Route.DataCompare) || `${Route.DataCompare}`;
addTabPage(Route.DataCompare, {node: ContentByRoute(Route.DataCompare, undefined, {system: system})})
// 区分新建对比页面还是别的页面请求对比的情况
ipcRenderer.invoke("created-data-compare")
})
return () => {
ipcRenderer.removeAllListeners("main-container-add-compare")
}
}, [pageCache])
// Global Sending Function(全局发送功能|通过发送新增功能页面)
const addFuzzer = useMemoizedFn((res: any) => {
const {isHttps, request} = res || {}
if (request) {
const time = new Date().getTime().toString()
addTabPage(Route.HTTPFuzzer, {
time: time,
node:
ContentByRoute(
Route.HTTPFuzzer,
undefined,
{
isHttps: isHttps || false,
request: request || "",
system: system,
order: time
}
)
})
addFuzzerList(time, request || "", isHttps || false)
}
})
const addScanPort = useMemoizedFn((res: any) => {
const {URL = ""} = res || {}
if (URL) {
addTabPage(Route.Mod_ScanPort, {
node: ContentByRoute(Route.Mod_ScanPort, undefined, {scanportParams: URL})
})
}
})
const addBrute = useMemoizedFn((res: any) => {
const {URL = ""} = res || {}
if (URL) {
addTabPage(Route.Mod_Brute, {
node: ContentByRoute(Route.Mod_Brute, undefined, {bruteParams: URL})
})
}
})
// 发送到专项漏洞检测modal-show变量
const [bugTestShow, setBugTestShow] = useState<boolean>(false)
const [bugList, setBugList] = useState<BugInfoProps[]>([])
const [bugTestValue, setBugTestValue] = useState<BugInfoProps[]>([])
const [bugUrl, setBugUrl] = useState<string>("")
const addBugTest = useMemoizedFn((type: number, res?: any) => {
const {URL = ""} = res || {}
if (type === 1 && URL) {
setBugUrl(URL)
ipcRenderer.invoke("get-value", CustomBugList)
.then((res: any) => {
setBugList(res ? JSON.parse(res) : [])
setBugTestShow(true)
})
.catch(() => {
})
}
if (type === 2) {
const filter = pageCache.filter(item => item.route === Route.PoC)
if (filter.length === 0) {
addTabPage(Route.PoC)
setTimeout(() => {
ipcRenderer.invoke("send-to-bug-test", {type: bugTestValue, data: bugUrl})
setBugTestValue([])
setBugUrl("")
}, 300);
} else {
ipcRenderer.invoke("send-to-bug-test", {type: bugTestValue, data: bugUrl})
setCurrentTabKey(Route.PoC)
setBugTestValue([])
setBugUrl("")
}
}
})
const addYakRunning = useMemoizedFn((res: any) => {
const {name = "", code = ""} = res || {}
const filter = pageCache.filter(item => item.route === Route.YakScript)
if (!name || !code) return false
if ((filter || []).length === 0) {
addTabPage(Route.YakScript)
setTimeout(() => {
ipcRenderer.invoke("send-to-yak-running", {name, code})
}, 300);
} else {
ipcRenderer.invoke("send-to-yak-running", {name, code})
setCurrentTabKey(Route.YakScript)
}
})
useEffect(() => {
ipcRenderer.on("fetch-send-to-tab", (e, res: any) => {
const {type, data = {}} = res
if (type === "fuzzer") addFuzzer(data)
if (type === "scan-port") addScanPort(data)
if (type === "brute") addBrute(data)
if (type === "bug-test") addBugTest(1, data)
if (type === "plugin-store") addYakRunning(data)
})
return () => {
ipcRenderer.removeAllListeners("fetch-send-to-tab")
}
}, [])
// Tabs Bar 组件
const closeAllCache = useMemoizedFn(() => {
Modal.confirm({
title: "确定要关闭所有 Tabs?",
content: "这样将会关闭所有进行中的进程",
onOk: () => {
delFuzzerList(1)
setPageCache([])
}
})
})
const closeOtherCache = useMemoizedFn((route: string) => {
Modal.confirm({
title: "确定要关闭除此之外所有 Tabs?",
content: "这样将会关闭所有进行中的进程",
onOk: () => {
const arr = pageCache.filter((i) => i.route === route)
setPageCache(arr)
if (route === Route.HTTPFuzzer) delFuzzerList(1)
}
})
})
const bars = (props: any, TabBarDefault: any) => {
return (
<TabBarDefault
{...props}
children={(barNode: React.ReactElement) => {
return (
<DropdownMenu
menu={{
data: [
{key: "all", title: "关闭所有Tabs"},
{key: "other", title: "关闭其他Tabs"}
]
}}
dropdown={{trigger: ["contextMenu"]}}
onClick={(key) => {
switch (key) {
case "all":
closeAllCache()
break
case "other":
closeOtherCache(barNode.key as Route)
break
default:
break
}
}}
>
{barNode}
</DropdownMenu>
)
}}
/>
)
}
return (
<Layout className="yakit-main-layout">
<AutoSpin spinning={loading}>
<Header className="main-laytou-header">
<Row>
<Col span={8}>
<Space>
<div style={{marginLeft: 18, textAlign: "center", height: 60}}>
<Image src={YakLogoBanner} preview={false} width={130} style={{marginTop: 6}}/>
</div>
<Divider type={"vertical"}/>
<YakVersion/>
<YakitVersion/>
{!hideMenu && (
<Button
style={{marginLeft: 4, color: "#207ee8"}}
type={"ghost"}
ghost={true}
onClick={(e) => {
setCollapsed(!collapsed)
}}
icon={collapsed ? <MenuUnfoldOutlined/> : <MenuFoldOutlined/>}
/>
)}
<Button
style={{marginLeft: 4, color: "#207ee8"}}
type={"ghost"}
ghost={true}
onClick={(e) => {
updateMenuItems()
}}
icon={<ReloadOutlined/>}
/>
</Space>
</Col>
<Col span={16} style={{textAlign: "right", paddingRight: 28}}>
<PerformanceDisplay/>
<RiskStatsTag professionalMode={true}/>
<Space>
{/* {status?.isTLS ? <Tag color={"green"}>TLS:通信已加密</Tag> : <Tag color={"red"}>通信未加密</Tag>} */}
{status?.addr && <Tag color={"geekblue"}>{status?.addr}</Tag>}
{/* <Tag color={engineStatus === "ok" ? "green" : "red"}>Yak 引擎状态:{engineStatus}</Tag> */}
<ReversePlatformStatus/>
<Dropdown forceRender={true} overlay={<Menu>
<Menu.Item key={"update"}>
<AutoUpdateYakModuleButton/>
</Menu.Item>
<Menu.Item key={"reverse-global"}>
<ConfigGlobalReverseButton/>
</Menu.Item>
</Menu>} trigger={["click"]}>
<Button icon={<SettingOutlined/>}>
配置
</Button>
</Dropdown>
<Button type={"link"} danger={true} icon={<PoweroffOutlined/>} onClick={() => {
if (winCloseFlag) setWinCloseShow(true)
else {
success("退出当前 Yak 服务器成功")
setEngineStatus("error")
}
}}/>
</Space>
</Col>
</Row>
</Header>
<Content
style={{
margin: 12,
backgroundColor: "#fff",
overflow: "auto"
}}
>
<Layout style={{height: "100%", overflow: "hidden"}}>
{!hideMenu && (
<Sider
style={{backgroundColor: "#fff", overflow: "auto"}}
collapsed={collapsed}
>
<Spin spinning={loading}>
<Space
direction={"vertical"}
style={{
width: "100%"
}}
>
<Menu
theme={"light"}
style={{}}
selectedKeys={[]}
mode={"inline"}
onSelect={(e) => {
if (e.key === "ignore") return
const flag = pageCache.filter(item => item.route === (e.key as Route)).length === 0
if (flag) menuAddPage(e.key as Route)
else setCurrentTabKey(e.key)
}}
>
{menuItems.map((i) => {
if (i.Group === "UserDefined") {
i.Group = "社区插件"
}
return (
<Menu.SubMenu icon={<EllipsisOutlined/>} key={i.Group}
title={i.Group}>
{i.Items.map((item) => {
if (item.YakScriptId > 0) {
return (
<MenuItem icon={<EllipsisOutlined/>}
key={`plugin:${item.Group}:${item.YakScriptId}`}>
<Text
ellipsis={{tooltip: true}}>{item.Verbose}</Text>
</MenuItem>
)
}
return (
<MenuItem icon={<EllipsisOutlined/>}
key={`batch:${item.Group}:${item.Verbose}:${item.MenuItemId}`}>
<Text
ellipsis={{tooltip: true}}>{item.Verbose}</Text>
</MenuItem>
)
})}
</Menu.SubMenu>
)
})}
{(routeMenuData || []).map((i) => {
if (i.subMenuData) {
return (
<Menu.SubMenu icon={i.icon} key={i.key} title={i.label}>
{(i.subMenuData || []).map((subMenu) => {
return (
<MenuItem icon={subMenu.icon} key={subMenu.key}
disabled={subMenu.disabled}>
<Text
ellipsis={{tooltip: true}}>{subMenu.label}</Text>
</MenuItem>
)
})}
</Menu.SubMenu>
)
}
return (
<MenuItem icon={i.icon} key={i.key} disabled={i.disabled}>
{i.label}
</MenuItem>
)
})}
</Menu>
</Space>
</Spin>
</Sider>
)}
<Content style={{
overflow: "hidden",
backgroundColor: "#fff",
marginLeft: 12,
height: "100%",
display: "flex",
flexFlow: "column"
}}>
<div style={{
padding: 12,
paddingTop: 8,
overflow: "hidden",
flex: "1",
display: "flex",
flexFlow: "column"
}}>
{pageCache.length > 0 ? (
<Tabs
style={{display: "flex", flex: "1"}}
tabBarStyle={{marginBottom: 8}}
className='main-content-tabs yakit-layout-tabs'
activeKey={currentTabKey}
onChange={setCurrentTabKey}
size={"small"}
type={"editable-card"}
renderTabBar={(props, TabBarDefault) => {
return bars(props, TabBarDefault)
}}
hideAdd={true}
onTabClick={(key, e) => {
const divExisted = document.getElementById("yakit-cursor-menu")
if (divExisted) {
const div: HTMLDivElement = divExisted as HTMLDivElement
const unmountResult = ReactDOM.unmountComponentAtNode(div)
if (unmountResult && div.parentNode) {
div.parentNode.removeChild(div)
}
}
}}
>
{pageCache.map((i) => {
return (
<Tabs.TabPane
forceRender={true}
key={i.route}
tab={i.verbose}
closeIcon={
<Space>
<Popover
trigger={"click"}
title={"修改名称"}
content={
<>
<Input
size={"small"}
defaultValue={i.verbose}
onBlur={(e) => updateCacheVerbose(i.route, e.target.value)}
/>
</>
}
>
<EditOutlined className='main-container-cion'/>
</Popover>
<CloseOutlined
className='main-container-cion'
onClick={() => removePage(i.route)}
/>
</Space>
}
>
<div
style={{
overflowY: NoScrollRoutes.includes(i.route)
? "hidden"
: "auto",
overflowX: "hidden",
height: "100%",
maxHeight: "100%"
}}
>
{i.singleNode ? (
i.singleNode
) : (
<MainTabs
currentTabKey={currentTabKey}
tabType={i.route}
pages={i.multipleNode}
currentKey={i.multipleCurrentKey || ""}
isShowAdd={true}
setCurrentKey={(key, type) => {
setMultipleCurrentKey(key, type as Route)
}}
removePage={(key, type) => {
removeMultipleNodePage(key, type as Route)
}}
removeOtherPage={(key, type) => {
removeOtherMultipleNodePage(key, type as Route)
}}
onAddTab={() => menuAddPage(i.route)}
></MainTabs>
)}
</div>
</Tabs.TabPane>
)
})}
</Tabs>
) : (
<></>
)}
</div>
</Content>
</Layout>
</Content>
</AutoSpin>
<Modal
visible={winCloseShow}
onCancel={() => setWinCloseShow(false)}
footer={[
<Button key='link' onClick={() => setWinCloseShow(false)}>
取消
</Button>,
<Button key='back' type='primary' onClick={() => {
success("退出当前 Yak 服务器成功")
setEngineStatus("error")
}}>
退出
</Button>
]}
>
<div style={{height: 40}}>
<ExclamationCircleOutlined style={{fontSize: 22, color: "#faad14"}}/>
<span style={{fontSize: 18, marginLeft: 15}}>提示</span>
</div>
<p style={{fontSize: 15, marginLeft: 37}}>是否要退出yakit操作界面,一旦退出,界面内打开内容除fuzzer页外都会销毁</p>
<div style={{marginLeft: 37}}>
<Checkbox
defaultChecked={!winCloseFlag}
value={!winCloseFlag}
onChange={() => {
setWinCloseFlag(!winCloseFlag)
ipcRenderer.invoke("set-value", WindowsCloseFlag, false)
}}
></Checkbox>
<span style={{marginLeft: 8}}>不再出现该提示信息</span>
</div>
</Modal>
<Modal
visible={bugTestShow}
onCancel={() => setBugTestShow(false)}
footer={[
<Button key='link' onClick={() => setBugTestShow(false)}>
取消
</Button>,
<Button key='back' type='primary' onClick={() => {
if ((bugTestValue || []).length === 0) return failed("请选择类型后再次提交")
addBugTest(2)
setBugTestShow(false)
}}>
确定
</Button>
]}
>
<ItemSelects
item={{
label: "专项漏洞类型",
style: {marginTop: 20}
}}
select={{
allowClear: true,
data: BugList.concat(bugList) || [],
optText: "title",
optValue: "key",
value: (bugTestValue || [])[0]?.key,
onChange: (value, option: any) => setBugTestValue(value ? [{
key: option?.key,
title: option?.title
}] : [])
}}
></ItemSelects>
</Modal>
</Layout>
)
}
Example #19
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 #20
Source File: JumpToNamespace.tsx From GTAV-NativeDB with MIT License | 4 votes |
function JumpToNamespace({ namespaces, onNamespaceClicked }: Props) {
const namespaceArray = useMemo(() => Object.values(namespaces), [namespaces])
const [filter, setFilter] = useState('')
const filteredNamespaces = useMemo(
() => namespaceArray
.filter(ns => ns.name.toLowerCase()
.indexOf(filter.toLowerCase()) !== -1),
[filter, namespaceArray]
)
const [dialogOpen, setDialogOpen] = useState(false)
const handleFilterChange = useCallback((e: ChangeEvent<HTMLInputElement>) => {
setFilter(e.target.value)
}, [setFilter])
const handleDialogOpen = useCallback(() => {
setDialogOpen(true)
}, [setDialogOpen])
const handleDialogClose = useCallback(() => {
setDialogOpen(false)
setFilter('')
}, [setDialogOpen])
const handleNamespaceSelected = useCallback((namespace: string) => {
onNamespaceClicked(namespace)
handleDialogClose()
}, [handleDialogClose, onNamespaceClicked])
useSetAppBarSettings('JumpToNamespace', {
actions: [
{
text: 'Jump to namespace',
mobileIcon: JumpToNamespaceIcon,
buttonProps: {
onClick: handleDialogOpen
}
}
]
})
const onKeyDown = useCallback((e: KeyboardEvent<HTMLDivElement>) => {
if (e.key === 'Enter' && filter && filteredNamespaces) {
handleNamespaceSelected(filteredNamespaces[0].name)
e.preventDefault()
}
}, [handleNamespaceSelected, filter, filteredNamespaces])
useHotkeys('ctrl+g', () => {
handleDialogOpen()
}, {
filter: (event: globalThis.KeyboardEvent) => {
event.preventDefault()
return true
},
}, [handleDialogOpen])
return (
<Fragment>
<Dialog open={dialogOpen} onClose={handleDialogClose} maxWidth="xs" fullWidth>
<TextField
label="Filter"
variant="filled"
value={filter}
onChange={handleFilterChange}
onKeyDown={onKeyDown}
fullWidth
autoFocus
/>
<List sx={{ height: 200, overflowY: 'scroll' }}>
{filteredNamespaces.map(({ name, natives }, index) => (
<ListItem
key={name}
onClick={() => handleNamespaceSelected(name)}
selected={!!filter && index === 0}
button
>
<ListItemText
primary={name}
secondary={`${natives.length} ${natives.length === 1 ? 'native' : 'natives'}`}
/>
</ListItem>
))}
</List>
</Dialog>
</Fragment>
)
}
Example #21
Source File: Shortcuts.tsx From Notepad with MIT License | 4 votes |
function Shortcuts({ children }: { children: ReactNode }) {
const { drawPadRef } = useContext(DrawPadRefContext);
const { drawMode } = useContext(DrawModeContext);
useHotkeys(
"ctrl+z",
e => {
e.preventDefault();
e.stopPropagation();
if (drawMode) drawPadRef.undo();
},
undefined,
drawPadRef
);
useHotkeys(
"ctrl+y",
e => {
e.preventDefault();
e.stopPropagation();
if (drawMode) drawPadRef.clear();
},
undefined,
drawPadRef
);
useHotkeys(
"alt+n",
e => {
e.preventDefault();
e.stopPropagation();
newFile({});
},
{ enableOnTags: ["TEXTAREA"] }
);
useHotkeys(
"ctrl+shift+n",
e => {
e.preventDefault();
e.stopPropagation();
newFile({ newWindow: true });
},
{ enableOnTags: ["TEXTAREA"] }
);
useHotkeys(
"ctrl+e",
e => {
e.preventDefault();
e.stopPropagation();
window.close();
},
{ enableOnTags: ["TEXTAREA"] }
);
useHotkeys(
"ctrl+o",
e => {
e.preventDefault();
e.stopPropagation();
if (!drawMode) {
const openFileELem = document.getElementById("open-file");
openFileELem?.click();
}
},
{ enableOnTags: ["TEXTAREA"] }
);
useHotkeys(
"F5",
e => {
e.preventDefault();
e.stopPropagation();
insertTimeAndDate(
document.getElementById("text-area") as HTMLTextAreaElement
);
},
{ enableOnTags: ["TEXTAREA"] }
);
useHotkeys(
"ctrl+s",
e => {
e.preventDefault();
e.stopPropagation();
if (drawMode)
saveDrawAsImage(
drawPadRef?.getDataURL(
"image/png",
null,
drawPadRef.props.backgroundColor
),
"draw.png"
);
else {
const textAreaElem: HTMLTextAreaElement = document.getElementById(
"text-area"
) as HTMLTextAreaElement;
downloadFile(textAreaElem.value, "Untitled.txt", "text/plain");
}
},
{ enableOnTags: ["TEXTAREA"] }
);
return <div id="shortcuts">{children}</div>;
}
Example #22
Source File: TAQueueListDetail.tsx From office-hours with GNU General Public License v3.0 | 4 votes |
/**
* List and detail panel of the TA queue
*/
export default function TAQueueListDetail({
queueId,
courseId,
}: {
queueId: number;
courseId: number;
}): ReactElement {
const user = useProfile();
const [selectedQuestionId, setSelectedQuestionId] = useState<number>(null);
const { questions } = useQuestions(queueId);
const [isGrouping, setIsGrouping] = useState<boolean>(false);
const isSideBySide = useWindowWidth() >= SPLIT_DETAIL_BKPT;
const onSelectQuestion = (qId: number) => {
setSelectedQuestionId(qId);
setIsGrouping(false);
};
const helpingQuestions = questions?.questionsGettingHelp?.filter(
(q) => q.taHelped.id === user.id
);
const myGroup = questions?.groups.find(
(group) => group.creator.id === user.id
);
const groupedQuestions = myGroup ? myGroup.questions : [];
const allQuestionsList: Question[] = questions
? [
...helpingQuestions,
...questions.queue,
...questions.priorityQueue,
...questions.groups.flatMap((e) => e.questions),
]
: [];
const selectedQuestion = allQuestionsList.find(
(q) => q.id === selectedQuestionId
);
const navigateQuestions = (isUp: boolean) => {
const priorityAndWaitingQuestionIds = [
...questions.priorityQueue,
...questions.queue,
].map((question) => question.id);
const numOfQuestions = priorityAndWaitingQuestionIds.length;
if (numOfQuestions > 0) {
setSelectedQuestionId((prevId) => {
const addMinus = isUp ? -1 : 1;
const qIdIndex = priorityAndWaitingQuestionIds.indexOf(prevId);
const modulusMagic =
(((qIdIndex + addMinus) % numOfQuestions) + numOfQuestions) %
numOfQuestions;
return priorityAndWaitingQuestionIds[prevId ? modulusMagic : 0];
});
}
};
useHotkeys("up", () => navigateQuestions(true), [questions]);
useHotkeys("down", () => navigateQuestions(false), [questions]);
const hasUnresolvedRephraseAlert = questions?.unresolvedAlerts
?.map((payload) => (payload as RephraseQuestionPayload).questionId)
.includes(selectedQuestionId);
// set currentQuestion to null if it no longer exists in the queue
if (selectedQuestionId && !selectedQuestion) {
onSelectQuestion(null);
}
// set current question to first helping question if none is selected (used when help next is clicked)
if (!selectedQuestionId && helpingQuestions.length) {
onSelectQuestion(helpingQuestions[0].id);
}
if (!questions) {
return <Skeleton />;
}
if (allQuestionsList.length === 0) {
return (
<EmptyQueueInfo>
<NoQuestionsText>There are no questions in the queue</NoQuestionsText>
{!user.phoneNotifsEnabled && !user.desktopNotifsEnabled && (
<NotifReminderButton courseId={courseId} />
)}
</EmptyQueueInfo>
);
}
const list = (
<List>
<div data-cy="list-helping">
<TAQueueListSection
title={"Currently Helping"}
questions={helpingQuestions}
onClickQuestion={onSelectQuestion}
selectedQuestionId={selectedQuestionId}
/>
</div>
<div data-cy="list-group">
<TAQueueListSection
title="Group Students"
questions={groupedQuestions}
onClickQuestion={() => {
setIsGrouping(true);
setSelectedQuestionId(null);
}}
collapsible
emptyDisplay={
<EmptyGroupList
onClick={() => {
setIsGrouping(true);
setSelectedQuestionId(null);
}}
/>
}
/>
</div>
<div data-cy="list-priority">
<TAQueueListSection
title={
<span>
<Tooltip title="Students in the priority queue were at the top of the queue before for some reason (e.g. they were at the top but AFK, or a TA helped them previously, and then hit 'requeue student.' You should communicate with your fellow staff members to prioritize these students first.">
<PriorityQueueQuestionBubble />
</Tooltip>
Priority Queue
</span>
}
questions={questions.priorityQueue}
onClickQuestion={onSelectQuestion}
selectedQuestionId={selectedQuestionId}
collapsible
/>
</div>
<div data-cy="list-queue">
<TAQueueListSection
title="Waiting In Line"
questions={questions.queue}
onClickQuestion={onSelectQuestion}
selectedQuestionId={selectedQuestionId}
collapsible
showNumbers
/>
</div>
</List>
);
const detail = (
<Detail>
{selectedQuestion && (
<TAQueueDetail
courseId={courseId}
queueId={queueId}
question={selectedQuestion}
hasUnresolvedRephraseAlert={hasUnresolvedRephraseAlert}
/>
)}
{isGrouping && (
<TAGroupDetail
courseId={courseId}
queueId={queueId}
groupCreator={user}
/>
)}
</Detail>
);
if (isSideBySide) {
return (
<Container>
{list}
{detail}
</Container>
);
} else if (selectedQuestionId) {
return (
<Container>
<BackToQueue onClick={() => onSelectQuestion(null)}>
<span>
<ArrowLeftOutlined />
{" Back To Queue"}
</span>
</BackToQueue>
{detail}
</Container>
);
} else {
return <Container>{list}</Container>;
}
}
Example #23
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 #24
Source File: TAQueue.tsx From office-hours with GNU General Public License v3.0 | 4 votes |
export default function TAQueue({ qid, courseId }: TAQueueProps): ReactElement {
const user = useProfile();
const role = useRoleInCourse(courseId);
const { queue } = useQueue(qid);
const { questions, mutateQuestions } = useQuestions(qid);
const { isCheckedIn, isHelping } = useTAInQueueInfo(qid);
const [queueSettingsModal, setQueueSettingsModal] = useState(false);
const { course } = useCourse(courseId);
const staffCheckedIntoAnotherQueue = course?.queues.some(
(q) =>
q.id !== qid &&
q.staffList.some((staffMember) => staffMember.id === user?.id)
);
const nextQuestion =
questions?.priorityQueue[0] || // gets the first item of priority queue if it exists
questions?.queue?.find(
(question) => question.status === QuestionStatusKeys.Queued
);
const defaultMessage = useDefaultMessage();
const openTeams = useTeams(qid, nextQuestion?.creator.email, defaultMessage);
const helpNext = async () => {
await onHelpQuestion(nextQuestion.id);
mutateQuestions();
openTeams();
};
useHotkeys(
"shift+h",
() => {
if (isCheckedIn && nextQuestion && !isHelping) {
helpNext();
}
},
[isCheckedIn, nextQuestion, isHelping, qid, defaultMessage]
);
// TODO: figure out tooltips
if (queue) {
return (
<>
<Container>
<QueueInfoColumn
queueId={qid}
isStaff={true}
buttons={
<>
<EditQueueButton
data-cy="editQueue"
onClick={() => setQueueSettingsModal(true)}
>
Edit Queue Details
</EditQueueButton>
<Tooltip
title={!isCheckedIn && "You must check in to help students!"}
>
<HelpNextButton
onClick={helpNext}
disabled={!isCheckedIn || !nextQuestion || isHelping}
data-cy="help-next"
>
Help Next
</HelpNextButton>
</Tooltip>
<div style={{ marginBottom: "12px" }}>
<Tooltip
title={
queue.isDisabled && "Cannot check into a disabled queue!"
}
>
<TACheckinButton
courseId={courseId}
room={queue?.room}
disabled={
staffCheckedIntoAnotherQueue ||
isHelping ||
(queue.isProfessorQueue && role !== Role.PROFESSOR) ||
queue.isDisabled
}
state={isCheckedIn ? "CheckedIn" : "CheckedOut"}
block
/>
</Tooltip>
</div>
</>
}
/>
<MiddleSpacer />
{user && questions && (
<TAQueueListDetail queueId={qid} courseId={courseId} />
)}
</Container>
<EditQueueModal
queueId={qid}
visible={queueSettingsModal}
onClose={() => setQueueSettingsModal(false)}
/>
</>
);
} else {
return <div />;
}
}
Example #25
Source File: QuestionForm.tsx From office-hours with GNU General Public License v3.0 | 4 votes |
export default function QuestionForm({
visible,
question,
leaveQueue,
finishQuestion,
position,
cancel,
}: QuestionFormProps): ReactElement {
const [storageQuestion, setStoredQuestion] = useLocalStorage(
"draftQuestion",
null
);
const router = useRouter();
const courseId = router.query["cid"];
const drafting = question?.status === OpenQuestionStatus.Drafting;
const helping = question?.status === OpenQuestionStatus.Helping;
const [questionTypeInput, setQuestionTypeInput] = useState<QuestionType>(
question?.questionType || null
);
const [questionText, setQuestionText] = useState<string>(
question?.text || ""
);
const [questionGroupable, setQuestionGroupable] = useState<boolean>(
question?.groupable !== undefined && question?.groupable
);
useEffect(() => {
if (question && !visible) {
setQuestionText(question.text);
setQuestionTypeInput(question.questionType);
}
}, [question, visible]);
// on question type change, update the question type state
const onCategoryChange = (e: RadioChangeEvent) => {
setQuestionTypeInput(e.target.value);
const questionFromStorage = storageQuestion ?? {};
setStoredQuestion({
id: question?.id,
...questionFromStorage,
questionType: e.target.value,
});
};
// on question text change, update the question text state
const onQuestionTextChange = (
event: React.ChangeEvent<HTMLTextAreaElement>
) => {
setQuestionText(event.target.value);
const questionFromStorage = storageQuestion ?? {};
setStoredQuestion({
id: question?.id,
...questionFromStorage,
text: event.target.value,
});
};
// on question groupable change, update the question groupable state
const onGroupableChange = (e: RadioChangeEvent) => {
setQuestionGroupable(e.target.value);
const questionFromStorage = storageQuestion ?? {};
setStoredQuestion({
id: question?.id,
...questionFromStorage,
groupable: e.target.value,
});
};
// on button submit click, conditionally choose to go back to the queue
const onClickSubmit = () => {
if (questionTypeInput && questionText && questionText !== "") {
finishQuestion(
questionText,
questionTypeInput,
questionGroupable,
router,
Number(courseId)
);
}
};
useHotkeys("enter", () => onClickSubmit(), { enableOnTags: ["TEXTAREA"] }, [
questionTypeInput,
questionText,
questionGroupable,
router,
courseId,
]);
return (
<Modal
visible={visible}
closable={true}
onCancel={() => {
setStoredQuestion(question);
cancel();
}}
title={drafting ? "Describe your question" : "Edit your question"}
footer={
<div>
{drafting ? (
<FormButton danger onClick={leaveQueue}>
Leave Queue
</FormButton>
) : (
<FormButton onClick={cancel}>Cancel</FormButton>
)}
<SaveChangesButton
data-cy="finishQuestion"
type="primary"
disabled={
!questionTypeInput || !questionText || questionText === ""
}
onClick={onClickSubmit}
>
{drafting ? "Finish" : "Save Changes"}
</SaveChangesButton>
</div>
}
>
<Container>
{drafting && (
<Alert
style={{ marginBottom: "32px" }}
message={`You are currently ${toOrdinal(position)} in queue`}
description="Your spot in queue has been temporarily reserved. Please describe your question to finish joining the queue."
type="success"
showIcon
/>
)}
{helping && (
<Alert
style={{ marginBottom: "32px" }}
message={`A TA is coming to help you`}
description="Please click 'Save Changes' to submit what you've filled out"
type="info"
showIcon
/>
)}
<QuestionText>
What category does your question fall under?
</QuestionText>
<Radio.Group
value={questionTypeInput}
onChange={onCategoryChange}
buttonStyle="solid"
style={{ marginBottom: 48 }}
>
<Radio.Button value={QuestionType.Concept}>Concept</Radio.Button>
<Radio.Button value={QuestionType.Clarification}>
Clarification
</Radio.Button>
<Radio.Button value={QuestionType.Testing}>Testing</Radio.Button>
<Radio.Button value={QuestionType.Bug}>Bug</Radio.Button>
<Radio.Button value={QuestionType.Setup}>Setup</Radio.Button>
<Radio.Button value={QuestionType.Other}>Other</Radio.Button>
</Radio.Group>
<QuestionText>What do you need help with?</QuestionText>
<Input.TextArea
data-cy="questionText"
value={questionText}
placeholder="I’m having trouble understanding list abstractions, particularly in Assignment 5."
autoSize={{ minRows: 3, maxRows: 6 }}
onChange={onQuestionTextChange}
/>
<QuestionCaption>
Be as descriptive and specific as possible in your answer. Your name
will be hidden to other students, but your question will be visible so
don't frame your question in a way that gives away the answer.
</QuestionCaption>
<QuestionText>
Would you like the option of being helped in a group session?
</QuestionText>
<Radio.Group
value={questionGroupable}
onChange={onGroupableChange}
style={{ marginBottom: 5 }}
>
<Radio value={true}>Yes</Radio>
<Radio value={false}>No</Radio>
</Radio.Group>
<QuestionCaption>
Clicking Yes may result in a shorter wait time if others have the same
question as you.
</QuestionCaption>
</Container>
</Modal>
);
}
Example #26
Source File: SQLEditor.tsx From datart with Apache License 2.0 | 4 votes |
SQLEditor = memo(() => {
const { actions } = useViewSlice();
const dispatch = useDispatch();
const {
editorInstance,
editorCompletionItemProviderRef,
setEditor,
initActions,
} = useContext(EditorContext);
const { showSaveForm } = useContext(SaveFormContext);
const id = useSelector<RootState>(state =>
selectCurrentEditingViewAttr(state, { name: 'id' }),
) as string;
const script = useSelector<RootState>(state =>
selectCurrentEditingViewAttr(state, { name: 'script' }),
) as string;
const stage = useSelector<RootState>(state =>
selectCurrentEditingViewAttr(state, { name: 'stage' }),
) as ViewViewModelStages;
const status = useSelector<RootState>(state =>
selectCurrentEditingViewAttr(state, { name: 'status' }),
) as ViewStatus;
const theme = useSelector(selectThemeKey);
const viewsData = useSelector(selectViews);
const t = useI18NPrefix('view.editor');
const run = useCallback(() => {
const fragment = editorInstance
?.getModel()
?.getValueInRange(editorInstance.getSelection()!);
dispatch(runSql({ id, isFragment: !!fragment }));
}, [dispatch, id, editorInstance]);
const save = useCallback(
(resolve?) => {
dispatch(saveView({ resolve }));
},
[dispatch],
);
const callSave = useCallback(() => {
if (
status !== ViewStatus.Archived &&
stage === ViewViewModelStages.Saveable
) {
if (isNewView(id)) {
showSaveForm({
type: CommonFormTypes.Edit,
visible: true,
parentIdLabel: t('folder'),
initialValues: {
name: '',
parentId: '',
config: {},
},
onSave: (values, onClose) => {
let index = getInsertedNodeIndex(values, viewsData);
dispatch(
actions.changeCurrentEditingView({
...values,
parentId: values.parentId || null,
index,
}),
);
save(onClose);
},
});
} else {
save();
}
}
}, [dispatch, actions, stage, status, id, save, showSaveForm, viewsData, t]);
const editorWillMount = useCallback(
editor => {
editor.languages.register({ id: 'sql' });
editor.languages.setMonarchTokensProvider('sql', language);
dispatch(
getEditorProvideCompletionItems({
resolve: getItems => {
const providerRef = editor.languages.registerCompletionItemProvider(
'sql',
{
provideCompletionItems: getItems,
},
);
if (editorCompletionItemProviderRef) {
editorCompletionItemProviderRef.current = providerRef;
}
},
}),
);
},
[dispatch, editorCompletionItemProviderRef],
);
const editorDidMount = useCallback(
(editor: monaco.editor.IStandaloneCodeEditor) => {
setEditor(editor);
// Removing the tooltip on the read-only editor
// https://github.com/microsoft/monaco-editor/issues/1742
const messageContribution = editor.getContribution(
'editor.contrib.messageController',
);
editor.onDidChangeCursorSelection(e => {
dispatch(
actions.changeCurrentEditingView({
fragment: editor.getModel()?.getValueInRange(e.selection),
}),
);
});
editor.onDidAttemptReadOnlyEdit(() => {
(messageContribution as any).showMessage(
t('readonlyTip'),
editor.getPosition(),
);
});
},
[setEditor, dispatch, actions, t],
);
useEffect(() => {
editorInstance?.layout();
return () => {
editorInstance?.dispose();
editorCompletionItemProviderRef?.current?.dispose();
};
}, [editorInstance]);
useEffect(() => {
return () => {
setEditor(void 0);
};
}, [setEditor]);
useEffect(() => {
initActions({ onRun: run, onSave: callSave });
}, [initActions, run, callSave]);
useEffect(() => {
editorInstance?.addCommand(
monaco.KeyMod.CtrlCmd | monaco.KeyCode.Enter,
run,
);
editorInstance?.addCommand(
monaco.KeyMod.CtrlCmd | monaco.KeyCode.KEY_S,
callSave,
);
}, [editorInstance, run, callSave]);
useHotkeys(
'ctrl+enter,command+enter',
() => {
run();
},
[run],
);
useHotkeys(
'ctrl+s,command+s',
e => {
e.preventDefault();
callSave();
},
[dispatch, callSave],
);
const debouncedEditorChange = useMemo(() => {
const editorChange = script => {
dispatch(actions.changeCurrentEditingView({ script }));
};
return debounce(editorChange, 200);
}, [dispatch, actions]);
return (
<EditorWrapper
className={classnames({
archived: status === ViewStatus.Archived,
})}
>
<MonacoEditor
value={script}
language="sql"
theme={`vs-${theme}`}
options={{
fontSize: FONT_SIZE_BASE * 0.875,
minimap: { enabled: false },
readOnly: status === ViewStatus.Archived,
}}
onChange={debouncedEditorChange}
editorWillMount={editorWillMount}
editorDidMount={editorDidMount}
/>
</EditorWrapper>
);
})
Example #27
Source File: TestDetailsModal.tsx From frontend with Apache License 2.0 | 4 votes |
TestDetailsModal: React.FunctionComponent<{
testRun: TestRun;
touched: boolean;
handleClose: () => void;
}> = ({ testRun, touched, handleClose }) => {
const classes = useStyles();
const navigate = useNavigate();
const { enqueueSnackbar } = useSnackbar();
const testRunDispatch = useTestRunDispatch();
const stageWidth = (window.innerWidth / 2) * 0.8;
const stageHeigth = window.innerHeight * 0.6;
const stageScaleBy = 1.2;
const [stageScale, setStageScale] = React.useState(1);
const [stagePos, setStagePos] = React.useState(defaultStagePos);
const [stageInitPos, setStageInitPos] = React.useState(defaultStagePos);
const [stageOffset, setStageOffset] = React.useState(defaultStagePos);
const [processing, setProcessing] = React.useState(false);
const [isDrawMode, setIsDrawMode] = useState(false);
const [valueOfIgnoreOrCompare, setValueOfIgnoreOrCompare] = useState(
"Ignore Areas"
);
const [isDiffShown, setIsDiffShown] = useState(false);
const [selectedRectId, setSelectedRectId] = React.useState<string>();
const [ignoreAreas, setIgnoreAreas] = React.useState<IgnoreArea[]>([]);
const [applyIgnoreDialogOpen, setApplyIgnoreDialogOpen] = React.useState(
false
);
const toggleApplyIgnoreDialogOpen = () => {
setApplyIgnoreDialogOpen(!applyIgnoreDialogOpen);
};
const [image, imageStatus] = useImage(
staticService.getImage(testRun.imageName)
);
const [baselineImage, baselineImageStatus] = useImage(
staticService.getImage(testRun.baselineName)
);
const [diffImage, diffImageStatus] = useImage(
staticService.getImage(testRun.diffName)
);
const applyIgnoreAreaText =
"Apply selected ignore area to all images in this build.";
React.useEffect(() => {
fitStageToScreen();
// eslint-disable-next-line
}, [image]);
React.useEffect(() => {
setIsDiffShown(!!testRun.diffName);
}, [testRun.diffName]);
React.useEffect(() => {
setIgnoreAreas(JSON.parse(testRun.ignoreAreas));
}, [testRun]);
const isImageSizeDiffer = React.useMemo(
() =>
testRun.baselineName &&
testRun.imageName &&
(image?.height !== baselineImage?.height ||
image?.width !== baselineImage?.width),
[image, baselineImage, testRun.baselineName, testRun.imageName]
);
const handleIgnoreAreaChange = (ignoreAreas: IgnoreArea[]) => {
setIgnoreAreas(ignoreAreas);
testRunDispatch({
type: "touched",
payload: testRun.ignoreAreas !== JSON.stringify(ignoreAreas),
});
};
const removeSelection = (event: KonvaEventObject<MouseEvent>) => {
// deselect when clicked not on Rect
const isRectClicked = event.target.className === "Rect";
if (!isRectClicked) {
setSelectedRectId(undefined);
}
};
const deleteIgnoreArea = (id: string) => {
handleIgnoreAreaChange(ignoreAreas.filter((area) => area.id !== id));
setSelectedRectId(undefined);
};
const saveTestRun = (ignoreAreas: IgnoreArea[], successMessage: string) => {
testRunService
.updateIgnoreAreas({
ids: [testRun.id],
ignoreAreas,
})
.then(() => {
enqueueSnackbar(successMessage, {
variant: "success",
});
})
.catch((err) =>
enqueueSnackbar(err, {
variant: "error",
})
);
};
const saveIgnoreAreasOrCompareArea = () => {
if (valueOfIgnoreOrCompare.includes("Ignore")) {
saveTestRun(ignoreAreas, "Ignore areas are updated.");
} else {
const invertedIgnoreAreas = invertIgnoreArea(
image!.width,
image!.height,
head(ignoreAreas)
);
handleIgnoreAreaChange(invertedIgnoreAreas);
saveTestRun(
invertedIgnoreAreas,
"Selected area has been inverted to ignore areas and saved."
);
}
testRunDispatch({ type: "touched", payload: false });
};
const onIgnoreOrCompareSelectChange = (value: string) => {
if (value.includes("Compare")) {
setValueOfIgnoreOrCompare("Compare Area");
} else {
setValueOfIgnoreOrCompare("Ignore Areas");
}
};
const setOriginalSize = () => {
setStageScale(1);
resetPositioin();
};
const fitStageToScreen = () => {
const scale = image
? Math.min(
stageWidth < image.width ? stageWidth / image.width : 1,
stageHeigth < image.height ? stageHeigth / image.height : 1
)
: 1;
setStageScale(scale);
resetPositioin();
};
const resetPositioin = () => {
setStagePos(defaultStagePos);
setStageOffset(defaultStagePos);
};
const applyIgnoreArea = () => {
let newIgnoreArea = ignoreAreas.find((area) => selectedRectId! === area.id);
if (newIgnoreArea) {
setProcessing(true);
testRunService
.getList(testRun.buildId)
.then((testRuns: TestRun[]) => {
let allIds = testRuns.map((item) => item.id);
let data: UpdateIgnoreAreaDto = {
ids: allIds,
ignoreAreas: [newIgnoreArea!],
};
testRunService.addIgnoreAreas(data).then(() => {
setProcessing(false);
setSelectedRectId(undefined);
enqueueSnackbar(
"Ignore areas are updated in all images in this build.",
{
variant: "success",
}
);
});
})
.catch((error) => {
enqueueSnackbar("There was an error : " + error, {
variant: "error",
});
setProcessing(false);
});
} else {
enqueueSnackbar(
"There was an error determining which ignore area to apply.",
{ variant: "error" }
);
}
};
useHotkeys(
"d",
() => !!testRun.diffName && setIsDiffShown((isDiffShown) => !isDiffShown),
[testRun.diffName]
);
useHotkeys("ESC", handleClose, [handleClose]);
return (
<React.Fragment>
<AppBar position="sticky">
<Toolbar>
<Grid container justifyContent="space-between">
<Grid item>
<Typography variant="h6">{testRun.name}</Typography>
</Grid>
{testRun.diffName && (
<Grid item>
<Tooltip title={"Hotkey: D"}>
<Switch
checked={isDiffShown}
onChange={() => setIsDiffShown(!isDiffShown)}
name="Toggle diff"
/>
</Tooltip>
</Grid>
)}
{(testRun.status === TestStatus.unresolved ||
testRun.status === TestStatus.new) && (
<Grid item>
<ApproveRejectButtons testRun={testRun} />
</Grid>
)}
<Grid item>
<IconButton color="inherit" onClick={handleClose}>
<Close />
</IconButton>
</Grid>
</Grid>
</Toolbar>
</AppBar>
{processing && <LinearProgress />}
<Box m={1}>
<Grid container alignItems="center">
<Grid item xs={12}>
<Grid container alignItems="center">
<Grid item>
<TestRunDetails testRun={testRun} />
</Grid>
{isImageSizeDiffer && (
<Grid item>
<Tooltip
title={
"Image height/width differ from baseline! Cannot calculate diff!"
}
>
<IconButton>
<WarningRounded color="secondary" />
</IconButton>
</Tooltip>
</Grid>
)}
</Grid>
</Grid>
<Grid item>
<Grid container alignItems="center" spacing={2}>
<Grid item>
<Select
id="area-select"
labelId="areaSelect"
value={valueOfIgnoreOrCompare}
onChange={(event) =>
onIgnoreOrCompareSelectChange(event.target.value as string)
}
>
{["Ignore Areas", "Compare Area"].map((eachItem) => (
<MenuItem key={eachItem} value={eachItem}>
{eachItem}
</MenuItem>
))}
</Select>
</Grid>
<Grid item>
<ToggleButton
value={"drawMode"}
selected={isDrawMode}
onClick={() => {
setIsDrawMode(!isDrawMode);
}}
>
<Add />
</ToggleButton>
</Grid>
<Grid item>
<IconButton
disabled={!selectedRectId || ignoreAreas.length === 0}
onClick={() =>
selectedRectId && deleteIgnoreArea(selectedRectId)
}
>
<Delete />
</IconButton>
</Grid>
<Tooltip title="Clears all ignore areas." aria-label="reject">
<Grid item>
<IconButton
disabled={ignoreAreas.length === 0}
onClick={() => {
handleIgnoreAreaChange([]);
}}
>
<LayersClear />
</IconButton>
</Grid>
</Tooltip>
<Tooltip
title={applyIgnoreAreaText}
aria-label="apply ignore area"
>
<Grid item>
<IconButton
disabled={!selectedRectId || ignoreAreas.length === 0}
onClick={() => toggleApplyIgnoreDialogOpen()}
>
<Collections />
</IconButton>
</Grid>
</Tooltip>
<Grid item>
<IconButton
disabled={!touched}
onClick={() => saveIgnoreAreasOrCompareArea()}
>
<Save />
</IconButton>
</Grid>
</Grid>
</Grid>
<Grid item>
<Button
color="primary"
disabled={!testRun.testVariationId}
onClick={() => {
navigate(
`${routes.VARIATION_DETAILS_PAGE}/${testRun.testVariationId}`
);
}}
>
Baseline history
</Button>
</Grid>
<Grid item>
<CommentsPopper
text={testRun.comment}
onSave={(comment) =>
testRunService
.update(testRun.id, { comment })
.then(() =>
enqueueSnackbar("Comment updated", {
variant: "success",
})
)
.catch((err) =>
enqueueSnackbar(err, {
variant: "error",
})
)
}
/>
</Grid>
</Grid>
</Box>
<Box
overflow="hidden"
minHeight="65%"
className={classes.drawAreaContainer}
>
<Grid container style={{ height: "100%" }}>
<Grid item xs={6} className={classes.drawAreaItem}>
<DrawArea
type="Baseline"
imageName={testRun.baselineName}
branchName={testRun.baselineBranchName}
imageState={[baselineImage, baselineImageStatus]}
ignoreAreas={[]}
tempIgnoreAreas={[]}
setIgnoreAreas={handleIgnoreAreaChange}
selectedRectId={selectedRectId}
setSelectedRectId={setSelectedRectId}
onStageClick={removeSelection}
stageScaleState={[stageScale, setStageScale]}
stagePosState={[stagePos, setStagePos]}
stageInitPosState={[stageInitPos, setStageInitPos]}
stageOffsetState={[stageOffset, setStageOffset]}
drawModeState={[false, setIsDrawMode]}
/>
</Grid>
<Grid item xs={6} className={classes.drawAreaItem}>
{isDiffShown ? (
<DrawArea
type="Diff"
imageName={testRun.diffName}
branchName={testRun.branchName}
imageState={[diffImage, diffImageStatus]}
ignoreAreas={ignoreAreas}
tempIgnoreAreas={JSON.parse(testRun.tempIgnoreAreas)}
setIgnoreAreas={handleIgnoreAreaChange}
selectedRectId={selectedRectId}
setSelectedRectId={setSelectedRectId}
onStageClick={removeSelection}
stageScaleState={[stageScale, setStageScale]}
stagePosState={[stagePos, setStagePos]}
stageInitPosState={[stageInitPos, setStageInitPos]}
stageOffsetState={[stageOffset, setStageOffset]}
drawModeState={[isDrawMode, setIsDrawMode]}
/>
) : (
<DrawArea
type="Image"
imageName={testRun.imageName}
branchName={testRun.branchName}
imageState={[image, imageStatus]}
ignoreAreas={ignoreAreas}
tempIgnoreAreas={JSON.parse(testRun.tempIgnoreAreas)}
setIgnoreAreas={handleIgnoreAreaChange}
selectedRectId={selectedRectId}
setSelectedRectId={setSelectedRectId}
onStageClick={removeSelection}
stageScaleState={[stageScale, setStageScale]}
stagePosState={[stagePos, setStagePos]}
stageInitPosState={[stageInitPos, setStageInitPos]}
stageOffsetState={[stageOffset, setStageOffset]}
drawModeState={[isDrawMode, setIsDrawMode]}
/>
)}
</Grid>
</Grid>
</Box>
<ScaleActionsSpeedDial
onZoomInClick={() => setStageScale(stageScale * stageScaleBy)}
onZoomOutClick={() => setStageScale(stageScale / stageScaleBy)}
onOriginalSizeClick={setOriginalSize}
onFitIntoScreenClick={fitStageToScreen}
/>
<BaseModal
open={applyIgnoreDialogOpen}
title={applyIgnoreAreaText}
submitButtonText={"Yes"}
onCancel={toggleApplyIgnoreDialogOpen}
content={
<Typography>
{`All images in the current build will be re-compared with new ignore area taken into account. Are you sure?`}
</Typography>
}
onSubmit={() => {
toggleApplyIgnoreDialogOpen();
applyIgnoreArea();
}}
/>
</React.Fragment>
);
}
Example #28
Source File: DetailsPanel.tsx From phosphor-home with MIT License | 4 votes |
DetailsPanel: React.FC<InfoPanelProps> = (props) => {
const { index, spans, isDark, entry } = props;
const { name, Icon, categories, tags } = entry;
const weight = useRecoilValue(iconWeightAtom);
const size = useRecoilValue(iconSizeAtom);
const color = useRecoilValue(iconColorAtom);
const setOpen = useSetRecoilState(iconPreviewOpenAtom);
const [copied, setCopied] = useTransientState<SnippetType | "SVG" | false>(
false,
2000
);
const ref = useRef<SVGSVGElement>(null);
useHotkeys("esc", () => setOpen(false));
useEffect(
() => ReactGA.event({ category: "Grid", action: "Details", label: name }),
[name]
);
const buttonBarStyle: React.CSSProperties = {
color: isDark ? "white" : buttonColor,
};
const snippetButtonStyle: React.CSSProperties =
weight === "duotone"
? { color: disabledColor, userSelect: "none" }
: { color: buttonColor };
const snippets = getCodeSnippets({
displayName: Icon.displayName!,
name,
weight,
size,
color,
});
const handleCopySnippet = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>,
type: SnippetType
) => {
event.currentTarget.blur();
setCopied(type);
const data = snippets[type];
data && void navigator.clipboard?.writeText(data);
};
const handleCopySVG = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
event.currentTarget.blur();
setCopied("SVG");
ref.current && void navigator.clipboard?.writeText(ref.current.outerHTML);
};
const handleDownloadSVG = (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
event.currentTarget.blur();
if (!ref.current?.outerHTML) return;
const blob = new Blob([ref.current.outerHTML]);
saveAs(blob, `${name}${weight === "regular" ? "" : `-${weight}`}.svg`);
};
const handleDownloadPNG = async (
event: React.MouseEvent<HTMLButtonElement, MouseEvent>
) => {
event.currentTarget.blur();
if (!ref.current?.outerHTML) return;
Svg2Png.save(
ref.current,
`${name}${weight === "regular" ? "" : `-${weight}`}.png`,
{ scaleX: 2.667, scaleY: 2.667 }
);
};
return (
<motion.section
className="info-box"
animate="open"
exit="collapsed"
variants={panelVariants}
style={{
order: index + (spans - (index % spans)),
color: isDark ? "white" : "black",
}}
>
<motion.div
initial="collapsed"
animate="open"
exit="collapsed"
variants={contentVariants}
className="icon-preview"
>
<Icon ref={ref} color={color} weight={weight} size={192} />
<p>{name}</p>
<TagCloud
name={name}
tags={Array.from(
new Set<string>([...categories, ...name.split("-"), ...tags])
)}
isDark={isDark}
/>
</motion.div>
<motion.div
initial="collapsed"
animate="open"
exit="collapsed"
variants={contentVariants}
className="icon-usage"
>
{renderedSnippets.map((type) => {
const isWeightSupported = supportsWeight({ type, weight });
return (
<div className="snippet" key={type}>
{type}
<pre
tabIndex={0}
style={isWeightSupported ? undefined : snippetButtonStyle}
>
<span>
{isWeightSupported
? snippets[type]
: "This weight is not yet supported"}
</span>
<button
title="Copy snippet"
onClick={(e) => handleCopySnippet(e, type)}
disabled={!isWeightSupported}
style={isWeightSupported ? undefined : snippetButtonStyle}
>
{copied === type ? (
<CheckCircle size={24} color={successColor} weight="fill" />
) : (
<Copy
size={24}
color={
isWeightSupported
? buttonColor
: snippetButtonStyle.color
}
weight="fill"
/>
)}
</button>
</pre>
</div>
);
})}
<div className="button-row">
<button style={buttonBarStyle} onClick={handleDownloadPNG}>
<Download size={32} color="currentColor" weight="fill" /> Download
PNG
</button>
<button style={buttonBarStyle} onClick={handleDownloadSVG}>
<Download size={32} color="currentColor" weight="fill" /> Download
SVG
</button>
<button style={buttonBarStyle} onClick={handleCopySVG}>
{copied === "SVG" ? (
<CheckCircle size={32} color={successColor} weight="fill" />
) : (
<Copy size={32} color="currentColor" weight="fill" />
)}
{copied === "SVG" ? "Copied!" : "Copy SVG"}
</button>
</div>
</motion.div>
<motion.span
initial="collapsed"
animate="open"
exit="collapsed"
variants={contentVariants}
title="Close"
>
<X
className="close-icon"
tabIndex={0}
color={buttonBarStyle.color}
size={32}
weight="fill"
onClick={() => setOpen(false)}
onKeyDown={(e) => {
e.key === "Enter" && setOpen(false);
}}
/>
</motion.span>
</motion.section>
);
}
Example #29
Source File: BuildingPanelEdit.tsx From condo with MIT License | 4 votes |
BuildingPanelEdit: React.FC<IBuildingPanelEditProps> = (props) => {
const intl = useIntl()
const SaveLabel = intl.formatMessage({ id: 'Save' })
const CancelLabel = intl.formatMessage({ id: 'Cancel' })
const ChangesSaved = intl.formatMessage({ id: 'ChangesSaved' })
const AllSectionsTitle = intl.formatMessage({ id: 'pages.condo.property.SectionSelect.AllTitle' })
const AllParkingSectionsTitle = intl.formatMessage({ id: 'pages.condo.property.ParkingSectionSelect.AllTitle' })
const SectionPrefixTitle = intl.formatMessage({ id: 'pages.condo.property.SectionSelect.OptionPrefix' })
const ParkingSectionPrefixTitle = intl.formatMessage({ id: 'pages.condo.property.ParkingSectionSelect.OptionPrefix' })
const MapValidationError = intl.formatMessage({ id: 'pages.condo.property.warning.modal.SameUnitNamesErrorMsg' })
const { mapValidationError, map, updateMap: updateFormField, handleSave, property } = props
const quickSave = Property.useUpdate({}, () => notification.success({
message: ChangesSaved,
placement: 'bottomRight',
}))
const debouncedQuickSave = useCallback(
debounce(() => quickSave({ map }, property), DEBOUNCE_TIMEOUT),
[map, property]
)
const { push, query: { id } } = useRouter()
const builderFormRef = useRef<HTMLDivElement | null>(null)
const [mapEdit, setMapEdit] = useState(new MapEdit(map, updateFormField))
const mode = mapEdit.editMode
const sections = mapEdit.sections
const address = get(property, 'address')
const quickSaveCallback = useCallback((event) => {
event.preventDefault()
if (mapEdit.validate()) {
debouncedQuickSave()
return
}
notification.error({
message: MapValidationError,
placement: 'bottomRight',
})
}, [debouncedQuickSave, mapEdit])
const saveCallback = useCallback(() => {
if (mapEdit.validate()) {
handleSave()
return
}
notification.error({
message: MapValidationError,
placement: 'bottomRight',
})
}, [handleSave, mapValidationError, mapEdit])
useHotkeys('ctrl+s', quickSaveCallback, [map, property])
const scrollToForm = () => {
if (builderFormRef && builderFormRef.current) {
const rect = builderFormRef.current.getBoundingClientRect()
const isVisible = (
rect.top >= 0 &&
rect.left >= 0 &&
rect.bottom <= (window.innerHeight || document.documentElement.clientHeight) &&
rect.right <= (window.innerWidth || document.documentElement.clientWidth)
)
if (!isVisible) {
builderFormRef.current.scrollIntoView()
}
}
}
const refresh = useCallback(() => {
setMapEdit(cloneDeep(mapEdit))
}, [mapEdit])
const changeMode = useCallback((mode) => {
mapEdit.editMode = mode
refresh()
}, [mapEdit, refresh])
const onCancel = useCallback(() => {
push(`/property/${id}`)
}, [id, push])
const menuClick = useCallback((event) => {
if (INSTANT_ACTIONS.includes(event.key)) {
if (isFunction(mapEdit[event.key])) mapEdit[event.key]()
return
}
changeMode(event.key)
}, [changeMode])
const onModalCancel = useCallback(() => {
changeMode(null)
}, [changeMode])
const onSelectSection = useCallback((id) => {
mapEdit.setVisibleSections(id)
refresh()
}, [mapEdit, refresh])
const onSelectParkingSection = useCallback((id) => {
mapEdit.setVisibleParkingSections(id)
refresh()
}, [mapEdit, refresh])
const onViewModeChange = useCallback((option) => {
mapEdit.viewMode = option.target.value
refresh()
}, [mapEdit, refresh])
const { isSmall } = useLayoutContext()
const showViewModeSelect = !mapEdit.isEmptySections && !mapEdit.isEmptyParking
const showSectionFilter = mapEdit.viewMode === MapViewMode.section && sections.length >= MIN_SECTIONS_TO_SHOW_FILTER
const showParkingFilter = mapEdit.viewMode === MapViewMode.parking && mapEdit.parking.length >= MIN_SECTIONS_TO_SHOW_FILTER
return (
<FullscreenWrapper mode={'edit'} className='fullscreen'>
<FullscreenHeader edit={true}>
<Row css={TopRowCss} justify='space-between'>
{address && (
<Col flex={0}>
<Space size={20}>
<AddressTopTextContainer>{address}</AddressTopTextContainer>
{showSectionFilter && (
<Select value={mapEdit.visibleSections} onSelect={onSelectSection}>
<Select.Option value={null} >{AllSectionsTitle}</Select.Option>
{
sections.map(section => (
<Select.Option key={section.id} value={section.id}>
{SectionPrefixTitle}{section.name}
</Select.Option>
))
}
</Select>
)}
{showParkingFilter && (
<Select value={mapEdit.visibleParkingSections} onSelect={onSelectParkingSection}>
<Select.Option value={null} >{AllParkingSectionsTitle}</Select.Option>
{
mapEdit.parking.map(parkingSection => (
<Select.Option key={parkingSection.id} value={parkingSection.id}>
{ParkingSectionPrefixTitle}{parkingSection.name}
</Select.Option>
))
}
</Select>
)}
</Space>
</Col>
)}
<Col flex={0}>
<Space size={20} style={{ flexDirection: isSmall ? 'column-reverse' : 'row' }}>
{
showViewModeSelect && (
<BuildingViewModeSelect
value={mapEdit.viewMode}
onChange={onViewModeChange}
disabled={mapEdit.editMode !== null}
/>
)
}
<BuildingEditTopMenu menuClick={menuClick} mapEdit={mapEdit} />
</Space>
</Col>
</Row>
<Row
style={UNIT_TYPE_ROW_STYLE}
hidden={mapEdit.viewMode === MapViewMode.parking}
>
<Col flex={0} style={UNIT_TYPE_COL_STYLE}>
<Row gutter={UNIT_TYPE_ROW_GUTTER}>
{mapEdit.getUnitTypeOptions()
.filter(unitType => unitType !== BuildingUnitSubType.Flat)
.map((unitType, unitTypeKey) => (
<Col key={unitTypeKey} flex={0}>
<UnitTypeLegendItem unitType={unitType}>
{intl.formatMessage({ id: `pages.condo.property.modal.unitType.${unitType}` })}
</UnitTypeLegendItem>
</Col>
))}
</Row>
</Col>
</Row>
<BuildingPanelTopModal
visible={!isNull(mode)}
title={!isNull(mode) ?
intl.formatMessage({ id: `pages.condo.property.modal.title.${mode}` })
: null
}
onClose={onModalCancel}
>
{
useMemo(() => ({
addSection: <AddSectionForm builder={mapEdit} refresh={refresh}/>,
addUnit: <UnitForm builder={mapEdit} refresh={refresh}/>,
editSection: <EditSectionForm builder={mapEdit} refresh={refresh}/>,
editUnit: <UnitForm builder={mapEdit} refresh={refresh}/>,
addParking: <AddParkingForm builder={mapEdit} refresh={refresh} />,
addParkingUnit: <ParkingUnitForm builder={mapEdit} refresh={refresh} />,
editParkingUnit: <ParkingUnitForm builder={mapEdit} refresh={refresh} />,
editParking: <EditParkingForm builder={mapEdit} refresh={refresh} />,
addSectionFloor: <AddSectionFloor builder={mapEdit} refresh={refresh} />,
}[mode] || null), [mode, mapEdit, refresh])
}
</BuildingPanelTopModal>
</FullscreenHeader>
<Row align='middle' style={{ height: '100%' }}>
<ChessBoard
builder={mapEdit}
refresh={refresh}
scrollToForm={scrollToForm}
isFullscreen
>
<Space size={20} align={'center'}>
<Button
key='submit'
onClick={saveCallback}
type='sberDefaultGradient'
disabled={!address}
>
{SaveLabel}
</Button>
<Button
key='cancel'
onClick={onCancel}
type='sberDefaultGradient'
secondary
>
{CancelLabel}
</Button>
{
mapValidationError ? (
<Typography.Paragraph type="danger" style={{ width: '100%', textAlign: 'center' }}>
{mapValidationError}
</Typography.Paragraph>
) : null
}
</Space>
</ChessBoard>
</Row>
</FullscreenWrapper>
)
}