diff --git a/src/components/CentralContainer/Sidebar/SidebarTabs/CustomTabPanel.css b/src/components/CentralContainer/Sidebar/SidebarTabs/CustomTabPanel.css index 8169315f..f6b1f720 100644 --- a/src/components/CentralContainer/Sidebar/SidebarTabs/CustomTabPanel.css +++ b/src/components/CentralContainer/Sidebar/SidebarTabs/CustomTabPanel.css @@ -2,12 +2,19 @@ padding: 0.75rem; } +.sidebar-tab-panel-container { + display: flex; + flex-direction: column; + height: 100%; +} + .sidebar-tab-panel-title-container { user-select: none; margin-bottom: 0.5rem !important; } .sidebar-tab-panel-title { + flex-grow: 1; font-size: 0.875rem !important; font-weight: 400 !important; text-transform: uppercase; diff --git a/src/components/CentralContainer/Sidebar/SidebarTabs/CustomTabPanel.tsx b/src/components/CentralContainer/Sidebar/SidebarTabs/CustomTabPanel.tsx index 9d1ab3e7..2b5d5bc2 100644 --- a/src/components/CentralContainer/Sidebar/SidebarTabs/CustomTabPanel.tsx +++ b/src/components/CentralContainer/Sidebar/SidebarTabs/CustomTabPanel.tsx @@ -1,6 +1,8 @@ import React from "react"; import { + Box, + ButtonGroup, DialogContent, DialogTitle, TabPanel, @@ -14,6 +16,7 @@ interface CustomTabPanelProps { children: React.ReactNode, tabName: string, title: string, + titleButtons?: React.ReactNode, } /** @@ -23,25 +26,40 @@ interface CustomTabPanelProps { * @param props.children * @param props.tabName * @param props.title + * @param props.titleButtons * @return */ -const CustomTabPanel = ({children, tabName, title}: CustomTabPanelProps) => { +const CustomTabPanel = ({ + children, + tabName, + title, + titleButtons, +}: CustomTabPanelProps) => { return ( - - - {title} - - - - {children} - + + + + {title} + + + {titleButtons} + + + + {children} + + ); }; diff --git a/src/components/CentralContainer/Sidebar/SidebarTabs/PanelTitleButton.css b/src/components/CentralContainer/Sidebar/SidebarTabs/PanelTitleButton.css new file mode 100644 index 00000000..27a464ec --- /dev/null +++ b/src/components/CentralContainer/Sidebar/SidebarTabs/PanelTitleButton.css @@ -0,0 +1,4 @@ +.tab-panel-title-button { + min-width: 0 !important; + min-height: 0 !important; +} diff --git a/src/components/CentralContainer/Sidebar/SidebarTabs/PanelTitleButton.tsx b/src/components/CentralContainer/Sidebar/SidebarTabs/PanelTitleButton.tsx new file mode 100644 index 00000000..b527a564 --- /dev/null +++ b/src/components/CentralContainer/Sidebar/SidebarTabs/PanelTitleButton.tsx @@ -0,0 +1,25 @@ +import { + IconButton, + IconButtonProps, +} from "@mui/joy"; + +import "./PanelTitleButton.css"; + + +/** + * Renders an IconButton for use in sidebar tab titles. + * + * @param props + * @return + */ +const PanelTitleButton = (props: IconButtonProps) => { + const {className, ...rest} = props; + return ( + + ); +}; + + +export default PanelTitleButton; diff --git a/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/Result.css b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/Result.css new file mode 100644 index 00000000..eac317d2 --- /dev/null +++ b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/Result.css @@ -0,0 +1,20 @@ +.result-button { + user-select: none; + + overflow-x: hidden; + + width: 100%; + padding-left: 12px; + + text-align: left; + text-overflow: ellipsis; + white-space: nowrap; +} + +.result-button:hover { + cursor: default; +} + +.result-button-text { + font-family: Inter, sans-serif !important; +} diff --git a/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/Result.tsx b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/Result.tsx new file mode 100644 index 00000000..e3d0576b --- /dev/null +++ b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/Result.tsx @@ -0,0 +1,70 @@ +import { + ListItemButton, + Typography, +} from "@mui/joy"; + +import {updateWindowUrlHashParams} from "../../../../../contexts/UrlContextProvider"; + +import "./Result.css"; + + +interface ResultProps { + logEventNum: number, + message: string, + matchRange: [number, number] +} + +const QUERY_RESULT_PREFIX_MAX_CHARACTERS = 20; + +/** + * Renders a query result as a button with a message, highlighting the first matching text range. + * + * @param props + * @param props.message + * @param props.matchRange A two-element array [begin, end) representing the indices of the matching + * text range. + * @param props.logEventNum + * @return + */ +const Result = ({logEventNum, message, matchRange}: ResultProps) => { + const [ + beforeMatch, + match, + afterMatch, + ] = [ + message.slice(0, matchRange[0]), + message.slice(...matchRange), + message.slice(matchRange[1]), + ]; + const handleResultButtonClick = () => { + updateWindowUrlHashParams({logEventNum}); + }; + + return ( + + + + {(QUERY_RESULT_PREFIX_MAX_CHARACTERS < beforeMatch.length) && "..."} + {beforeMatch.slice(-QUERY_RESULT_PREFIX_MAX_CHARACTERS)} + + + {match} + + {afterMatch} + + + ); +}; + + +export default Result; diff --git a/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/ResultsGroup.css b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/ResultsGroup.css new file mode 100644 index 00000000..2729ecdd --- /dev/null +++ b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/ResultsGroup.css @@ -0,0 +1,31 @@ +.results-group-summary-button { + cursor: default !important; + flex-direction: row-reverse !important; + gap: 2px !important; + padding-inline-start: 0 !important; +} + +.results-group-summary-container { + display: flex; + flex-grow: 1; +} + +.results-group-summary-text-container { + flex-grow: 1; + gap: 0.2rem; + align-items: center; +} + +.results-group-summary-count { + border-radius: 4px !important; +} + +.results-group-details { + margin-left: 1.5px !important; + /* stylelint-disable-next-line custom-property-pattern */ + border-left: 1px solid var(--joy-palette-neutral-outlinedBorder, #cdd7e1); +} + +.results-group-details-content { + padding-block: 0 !important; +} diff --git a/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/ResultsGroup.tsx b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/ResultsGroup.tsx new file mode 100644 index 00000000..28e1ce4a --- /dev/null +++ b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/ResultsGroup.tsx @@ -0,0 +1,114 @@ +import React, { + memo, + useEffect, + useState, +} from "react"; + +import { + Accordion, + AccordionDetails, + AccordionSummary, + Box, + Chip, + List, + Stack, + Typography, +} from "@mui/joy"; + +import DescriptionOutlinedIcon from "@mui/icons-material/DescriptionOutlined"; + +import {QueryResultsType} from "../../../../../typings/worker"; +import Result from "./Result"; + +import "./ResultsGroup.css"; + + +interface ResultsGroupProps { + isAllExpanded: boolean, + pageNum: number, + results: QueryResultsType[], +} + +/** + * Renders a group of results, where each group represents a list of results from a single page. + * + * @param props + * @param props.isAllExpanded + * @param props.pageNum + * @param props.results + * @return + */ +const ResultsGroup = memo(({ + isAllExpanded, + pageNum, + results, +}: ResultsGroupProps) => { + const [isExpanded, setIsExpanded] = useState(isAllExpanded); + + const handleAccordionChange = ( + _: React.SyntheticEvent, + newValue: boolean + ) => { + setIsExpanded(newValue); + }; + + // On `isAllExpanded` update, sync current results group's expand status. + useEffect(() => { + setIsExpanded(isAllExpanded); + }, [isAllExpanded]); + + return ( + + + + + + + {"Page "} + {pageNum} + + + + {results.length} + + + + + + {results.map((r, index) => ( + + ))} + + + + ); +}); + +ResultsGroup.displayName = "ResultsGroup"; + + +export default ResultsGroup; diff --git a/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/index.css b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/index.css new file mode 100644 index 00000000..0670f821 --- /dev/null +++ b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/index.css @@ -0,0 +1,40 @@ +.search-tab-container { + overflow-y: hidden; + display: flex; + flex-direction: column; + height: 100%; +} + +.query-input-box-with-progress { + /* JoyUI has a rounding issue when calculating the Textarea width, causing it to overflow its + container. */ + margin-right: 1px; +} + +.query-input-box { + flex-direction: row !important; + border-radius: 0 !important; +} + +.query-option-button { + font-family: Inter, sans-serif !important; +} + +.query-input-box-textarea { + width: 0; +} + +.query-input-box-end-decorator { + display: block !important; + margin-block-start: 0 !important; +} + +.query-input-box-linear-progress { + /* stylelint-disable-next-line custom-property-pattern */ + --LinearProgress-radius: 0 !important; +} + +.query-results { + overflow-y: auto; + flex-grow: 1; +} diff --git a/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/index.tsx b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/index.tsx new file mode 100644 index 00000000..631878f1 --- /dev/null +++ b/src/components/CentralContainer/Sidebar/SidebarTabs/SearchTabPanel/index.tsx @@ -0,0 +1,135 @@ +import React, { + useContext, + useState, +} from "react"; + +import { + AccordionGroup, + Box, + IconButton, + LinearProgress, + Textarea, + ToggleButtonGroup, +} from "@mui/joy"; + +import UnfoldLessIcon from "@mui/icons-material/UnfoldLess"; +import UnfoldMoreIcon from "@mui/icons-material/UnfoldMore"; + +import {StateContext} from "../../../../../contexts/StateContextProvider"; +import {UI_ELEMENT} from "../../../../../typings/states"; +import { + TAB_DISPLAY_NAMES, + TAB_NAME, +} from "../../../../../typings/tab"; +import {QUERY_PROGRESS_DONE} from "../../../../../typings/worker"; +import {isDisabled} from "../../../../../utils/states"; +import CustomTabPanel from "../CustomTabPanel"; +import PanelTitleButton from "../PanelTitleButton"; +import ResultsGroup from "./ResultsGroup"; + +import "./index.css"; + + +enum QUERY_OPTION { + IS_CASE_SENSITIVE = "isCaseSensitive", + IS_REGEX = "isRegex" +} + +/** + * Displays a panel for submitting queries and viewing query results. + * + * @return + */ +const SearchTabPanel = () => { + const {queryProgress, queryResults, startQuery, uiState} = useContext(StateContext); + const [isAllExpanded, setIsAllExpanded] = useState(true); + const [queryOptions, setQueryOptions] = useState([]); + + const handleQueryInputChange = (ev: React.ChangeEvent) => { + const isCaseSensitive = queryOptions.includes(QUERY_OPTION.IS_CASE_SENSITIVE); + const isRegex = queryOptions.includes(QUERY_OPTION.IS_REGEX); + startQuery(ev.target.value, isRegex, isCaseSensitive); + }; + const handleQueryOptionsChange = ( + _: React.MouseEvent, + newOptions: QUERY_OPTION[] + ) => { + setQueryOptions(newOptions); + }; + + return ( + { setIsAllExpanded((v) => !v); }}> + {isAllExpanded ? + : + } + + } + > + + + + + Aa + + + .* + + + } + slotProps={{ + textarea: {className: "query-input-box-textarea"}, + endDecorator: {className: "query-input-box-end-decorator"}, + }} + onChange={handleQueryInputChange}/> + + + + {Array.from(queryResults.entries()).map(([pageNum, results]) => ( + + ))} + + + + ); +}; + + +export default SearchTabPanel; diff --git a/src/components/CentralContainer/Sidebar/SidebarTabs/index.css b/src/components/CentralContainer/Sidebar/SidebarTabs/index.css index f9b4e2c3..51b03438 100644 --- a/src/components/CentralContainer/Sidebar/SidebarTabs/index.css +++ b/src/components/CentralContainer/Sidebar/SidebarTabs/index.css @@ -1,7 +1,8 @@ .sidebar-tabs { + overflow-y: hidden; flex-grow: 1; width: calc(100% - var(--ylv-panel-resize-handle-width)); - height: 100%; + height: calc(100vh - var(--ylv-menu-bar-height) - var(--ylv-status-bar-height)); } .sidebar-tab-list-spacing { diff --git a/src/components/CentralContainer/Sidebar/SidebarTabs/index.tsx b/src/components/CentralContainer/Sidebar/SidebarTabs/index.tsx index 9a09a4b0..2e7a0596 100644 --- a/src/components/CentralContainer/Sidebar/SidebarTabs/index.tsx +++ b/src/components/CentralContainer/Sidebar/SidebarTabs/index.tsx @@ -10,11 +10,13 @@ import { import SvgIcon from "@mui/material/SvgIcon"; import InfoOutlinedIcon from "@mui/icons-material/InfoOutlined"; +import SearchIcon from "@mui/icons-material/Search"; import SettingsOutlinedIcon from "@mui/icons-material/SettingsOutlined"; import {TAB_NAME} from "../../../../typings/tab"; import SettingsModal from "../../../modals/SettingsModal"; import FileInfoTabPanel from "./FileInfoTabPanel"; +import SearchTabPanel from "./SearchTabPanel"; import TabButton from "./TabButton"; import "./index.css"; @@ -28,6 +30,7 @@ const TABS_INFO_LIST: Readonly> = Object.freeze([ {tabName: TAB_NAME.FILE_INFO, Icon: InfoOutlinedIcon}, + {tabName: TAB_NAME.SEARCH, Icon: SearchIcon}, ]); interface SidebarTabsProps { @@ -93,6 +96,7 @@ const SidebarTabs = forwardRef(( onTabButtonClick={handleTabButtonClick}/> + void, @@ -85,6 +87,7 @@ const STATE_DEFAULT: Readonly = Object.freeze({ numPages: 0, onDiskFileSizeInBytes: 0, pageNum: 0, + queryProgress: QUERY_PROGRESS_INIT, queryResults: new Map(), uiState: UI_STATE.UNOPENED, @@ -234,24 +237,25 @@ const StateContextProvider = ({children}: StateContextProviderProps) => { const [exportProgress, setExportProgress] = useState>(STATE_DEFAULT.exportProgress); const [fileName, setFileName] = useState(STATE_DEFAULT.fileName); - const [uiState, setUiState] = useState(STATE_DEFAULT.uiState); const [logData, setLogData] = useState(STATE_DEFAULT.logData); const [numEvents, setNumEvents] = useState(STATE_DEFAULT.numEvents); const [numPages, setNumPages] = useState(STATE_DEFAULT.numPages); const [onDiskFileSizeInBytes, setOnDiskFileSizeInBytes] = useState(STATE_DEFAULT.onDiskFileSizeInBytes); const [pageNum, setPageNum] = useState(STATE_DEFAULT.pageNum); + const [queryProgress, setQueryProgress] = useState(STATE_DEFAULT.queryProgress); const [queryResults, setQueryResults] = useState(STATE_DEFAULT.queryResults); - const beginLineNumToLogEventNumRef = - useRef(STATE_DEFAULT.beginLineNumToLogEventNum); + const [uiState, setUiState] = useState(STATE_DEFAULT.uiState); // Refs + const beginLineNumToLogEventNumRef = + useRef(STATE_DEFAULT.beginLineNumToLogEventNum); const logEventNumRef = useRef(logEventNum); + const logExportManagerRef = useRef(null); + const mainWorkerRef = useRef(null); const numPagesRef = useRef(numPages); const pageNumRef = useRef(pageNum); const uiStateRef = useRef(uiState); - const logExportManagerRef = useRef(null); - const mainWorkerRef = useRef(null); const handleMainWorkerResp = useCallback((ev: MessageEvent) => { const {code, args} = ev.data; @@ -306,16 +310,22 @@ const StateContextProvider = ({children}: StateContextProviderProps) => { break; } case WORKER_RESP_CODE.QUERY_RESULT: - setQueryResults((v) => { - args.results.forEach((resultsPerPage, queryPageNum) => { - if (false === v.has(queryPageNum)) { - v.set(queryPageNum, []); - } - v.get(queryPageNum)?.push(...resultsPerPage); + setQueryProgress(args.progress); + if (QUERY_PROGRESS_INIT === args.progress) { + setQueryResults(STATE_DEFAULT.queryResults); + } else { + setQueryResults((v) => { + v = structuredClone(v); + args.results.forEach((resultsPerPage, queryPageNum) => { + if (false === v.has(queryPageNum)) { + v.set(queryPageNum, []); + } + v.get(queryPageNum)?.push(...resultsPerPage); + }); + + return v; }); - - return v; - }); + } break; default: console.error(`Unexpected ev.data: ${JSON.stringify(ev.data)}`); @@ -505,6 +515,7 @@ const StateContextProvider = ({children}: StateContextProviderProps) => { numPages: numPages, onDiskFileSizeInBytes: onDiskFileSizeInBytes, pageNum: pageNum, + queryProgress: queryProgress, queryResults: queryResults, uiState: uiState, diff --git a/src/services/LogFileManager/index.ts b/src/services/LogFileManager/index.ts index fc1faaba..10566999 100644 --- a/src/services/LogFileManager/index.ts +++ b/src/services/LogFileManager/index.ts @@ -1,6 +1,7 @@ -/* eslint max-lines: ["error", 400] */ +/* eslint max-lines: ["error", 450] */ import { Decoder, + DecodeResult, DecoderOptions, } from "../../typings/decoders"; import {MAX_V8_STRING_LENGTH} from "../../typings/js"; @@ -33,6 +34,8 @@ import { } from "./utils"; +const MAX_QUERY_RESULT_COUNT = 1_000; + /** * Class to manage the retrieval and decoding of a given log file. */ @@ -47,10 +50,12 @@ class LogFileManager { readonly #onDiskFileSizeInBytes: number; - readonly #onQueryResults: (queryResults: QueryResults) => void; + readonly #onQueryResults: (queryProgress: number, queryResults: QueryResults) => void; #decoder: Decoder; + #queryCount: number = 0; + /** * Private constructor for LogFileManager. This is not intended to be invoked publicly. * Instead, use LogFileManager.create() to create a new instance of the class. @@ -67,7 +72,7 @@ class LogFileManager { fileName: string, onDiskFileSizeInBytes: number, pageSize: number, - onQueryResults: (queryResults: QueryResults) => void, + onQueryResults: (queryProgress: number, queryResults: QueryResults) => void, }) { this.#decoder = decoder; this.#fileName = fileName; @@ -111,7 +116,7 @@ class LogFileManager { fileSrc: FileSrcType, pageSize: number, decoderOptions: DecoderOptions, - onQueryResults: (queryResults: QueryResults) => void, + onQueryResults: (queryProgress: number, queryResults: QueryResults) => void, ): Promise { const {fileName, fileData} = await loadFile(fileSrc); const decoder = await LogFileManager.#initDecoder(fileName, fileData, decoderOptions); @@ -287,6 +292,11 @@ class LogFileManager { */ startQuery (queryString: string, isRegex: boolean, isCaseSensitive: boolean): void { this.#queryId++; + this.#queryCount = 0; + + // Send an empty query result with 0 progress to the render to init the results variable + // because there could be results sent by previous task before `startQuery()` runs. + this.#onQueryResults(0, new Map()); // If the query string is empty, or there are no logs, return if ("" === queryString || 0 === this.#numEvents) { @@ -305,6 +315,45 @@ class LogFileManager { this.#queryChunkAndScheduleNext(this.#queryId, 0, queryRegex); } + /** + * Processes decoded log events and populates the results map with matched entries. + * + * @param decodedEvents + * @param queryRegex + * @param results The map to store query results. + */ + #processQueryDecodedEvents ( + decodedEvents: DecodeResult[], + queryRegex: RegExp, + results: QueryResults + ): void { + for (const [message, , , logEventNum] of decodedEvents) { + const matchResult = message.match(queryRegex); + if (null === matchResult || "number" !== typeof matchResult.index) { + continue; + } + + const pageNum = Math.ceil(logEventNum / this.#pageSize); + if (false === results.has(pageNum)) { + results.set(pageNum, []); + } + + results.get(pageNum)?.push({ + logEventNum: logEventNum, + message: message, + matchRange: [ + matchResult.index, + matchResult.index + matchResult[0].length, + ], + }); + + this.#queryCount++; + if (this.#queryCount >= MAX_QUERY_RESULT_COUNT) { + break; + } + } + } + /** * Queries a chunk of log events, sends the results, and schedules the next chunk query if more * log events remain. @@ -330,27 +379,22 @@ class LogFileManager { null !== this.#decoder.getFilteredLogEventMap() ); - decodedEvents?.forEach(([message, , , logEventNum]) => { - const matchResult = message.match(queryRegex); - if (null !== matchResult && "number" === typeof matchResult.index) { - const pageNum = Math.ceil(logEventNum / this.#pageSize); - if (false === results.has(pageNum)) { - results.set(pageNum, []); - } - results.get(pageNum)?.push({ - logEventNum: logEventNum, - message: message, - matchRange: [ - matchResult.index, - (matchResult.index + matchResult[0].length), - ], - }); - } - }); + if (null === decodedEvents) { + return; + } + + this.#processQueryDecodedEvents(decodedEvents, queryRegex, results); + + // The query progress takes the maximum of the progress based on the number of events + // queried over total log events, and the number of results over the maximum result limit. + const progress = Math.max( + chunkEndIdx / this.#numEvents, + this.#queryCount / MAX_QUERY_RESULT_COUNT + ); - this.#onQueryResults(results); + this.#onQueryResults(progress, results); - if (chunkEndIdx < this.#numEvents) { + if (chunkEndIdx < this.#numEvents && MAX_QUERY_RESULT_COUNT > this.#queryCount) { defer(() => { this.#queryChunkAndScheduleNext(queryId, chunkEndIdx, queryRegex); }); diff --git a/src/services/MainWorker.ts b/src/services/MainWorker.ts index 021353f3..ce58d1f9 100644 --- a/src/services/MainWorker.ts +++ b/src/services/MainWorker.ts @@ -43,10 +43,11 @@ const postResp = ( /** * Post a response for a chunk of query results. * + * @param queryProgress * @param queryResults */ -const onQueryResults = (queryResults: QueryResults) => { - postResp(WORKER_RESP_CODE.QUERY_RESULT, {results: queryResults}); +const onQueryResults = (queryProgress: number, queryResults: QueryResults) => { + postResp(WORKER_RESP_CODE.QUERY_RESULT, {progress: queryProgress, results: queryResults}); }; // eslint-disable-next-line no-warning-comments diff --git a/src/typings/states.ts b/src/typings/states.ts index 6a02064f..7b2517c3 100644 --- a/src/typings/states.ts +++ b/src/typings/states.ts @@ -42,6 +42,7 @@ enum UI_ELEMENT { NAVIGATION_BAR, OPEN_FILE_BUTTON, PROGRESS_BAR, + QUERY_INPUT_BOX, } type UiElementRow = { @@ -66,6 +67,7 @@ const UI_STATE_GRID: UiStateGrid = Object.freeze({ [UI_ELEMENT.NAVIGATION_BAR]: false, [UI_ELEMENT.OPEN_FILE_BUTTON]: true, [UI_ELEMENT.PROGRESS_BAR]: false, + [UI_ELEMENT.QUERY_INPUT_BOX]: false, }, [UI_STATE.FILE_LOADING]: { [UI_ELEMENT.DRAG_AND_DROP]: false, @@ -75,6 +77,7 @@ const UI_STATE_GRID: UiStateGrid = Object.freeze({ [UI_ELEMENT.NAVIGATION_BAR]: false, [UI_ELEMENT.OPEN_FILE_BUTTON]: false, [UI_ELEMENT.PROGRESS_BAR]: true, + [UI_ELEMENT.QUERY_INPUT_BOX]: false, }, [UI_STATE.FAST_LOADING]: { [UI_ELEMENT.DRAG_AND_DROP]: true, @@ -84,6 +87,7 @@ const UI_STATE_GRID: UiStateGrid = Object.freeze({ [UI_ELEMENT.NAVIGATION_BAR]: true, [UI_ELEMENT.OPEN_FILE_BUTTON]: true, [UI_ELEMENT.PROGRESS_BAR]: true, + [UI_ELEMENT.QUERY_INPUT_BOX]: false, }, [UI_STATE.SLOW_LOADING]: { [UI_ELEMENT.DRAG_AND_DROP]: false, @@ -93,6 +97,7 @@ const UI_STATE_GRID: UiStateGrid = Object.freeze({ [UI_ELEMENT.NAVIGATION_BAR]: false, [UI_ELEMENT.OPEN_FILE_BUTTON]: false, [UI_ELEMENT.PROGRESS_BAR]: false, + [UI_ELEMENT.QUERY_INPUT_BOX]: false, }, [UI_STATE.READY]: { [UI_ELEMENT.DRAG_AND_DROP]: true, @@ -102,6 +107,7 @@ const UI_STATE_GRID: UiStateGrid = Object.freeze({ [UI_ELEMENT.NAVIGATION_BAR]: true, [UI_ELEMENT.OPEN_FILE_BUTTON]: true, [UI_ELEMENT.PROGRESS_BAR]: false, + [UI_ELEMENT.QUERY_INPUT_BOX]: true, }, }); diff --git a/src/typings/tab.ts b/src/typings/tab.ts index b2e8b1bf..8f4ba857 100644 --- a/src/typings/tab.ts +++ b/src/typings/tab.ts @@ -1,6 +1,7 @@ enum TAB_NAME { NONE = "none", FILE_INFO = "fileInfo", + SEARCH = "search", SETTINGS = "settings", } @@ -10,6 +11,7 @@ enum TAB_NAME { const TAB_DISPLAY_NAMES: Record = Object.freeze({ [TAB_NAME.NONE]: "None", [TAB_NAME.FILE_INFO]: "File info", + [TAB_NAME.SEARCH]: "Search", [TAB_NAME.SETTINGS]: "Settings", }); diff --git a/src/typings/worker.ts b/src/typings/worker.ts index f589c706..334f1cb0 100644 --- a/src/typings/worker.ts +++ b/src/typings/worker.ts @@ -115,6 +115,9 @@ interface QueryResultsType { type QueryResults = Map; +const QUERY_PROGRESS_INIT = 0; +const QUERY_PROGRESS_DONE = 1; + type WorkerRespMap = { [WORKER_RESP_CODE.CHUNK_DATA]: { logs: string @@ -136,7 +139,10 @@ type WorkerRespMap = { numPages: number, pageNum: number, }, - [WORKER_RESP_CODE.QUERY_RESULT]: { results: QueryResults }, + [WORKER_RESP_CODE.QUERY_RESULT]: { + progress: number, + results: QueryResults + }, }; type WorkerReq = T extends keyof WorkerReqMap ? @@ -172,6 +178,8 @@ export { CURSOR_CODE, EMPTY_PAGE_RESP, EVENT_POSITION_ON_PAGE, + QUERY_PROGRESS_DONE, + QUERY_PROGRESS_INIT, WORKER_REQ_CODE, WORKER_RESP_CODE, }; @@ -183,6 +191,7 @@ export type { MainWorkerReqMessage, MainWorkerRespMessage, QueryResults, + QueryResultsType, WorkerReq, WorkerResp, };