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 vote down vote up
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&apos; 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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
/**
 * 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 vote down vote up
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 vote down vote up
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 vote down vote up
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&apos;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 vote down vote up
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 vote down vote up
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 vote down vote up
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 vote down vote up
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>
    )
}