Skip to content

Commit

Permalink
Add support for viewing search results in context for text logs (clp-…
Browse files Browse the repository at this point in the history
…text). (y-scope#489)
  • Loading branch information
junhaoliao authored and Jack Luo committed Dec 4, 2024
1 parent b96f5a1 commit e9d5b2b
Show file tree
Hide file tree
Showing 17 changed files with 1,000 additions and 108 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -786,6 +786,9 @@ def start_webui(instance_id: str, clp_config: CLPConfig, mounts: CLPDockerMounts
},
"public": {
"ClpStorageEngine": clp_config.package.storage_engine,
"LogViewerWebuiUrl": (
f"http://{clp_config.log_viewer_webui.host}:{clp_config.log_viewer_webui.port}",
),
},
}
meteor_settings = read_and_update_settings_json(settings_json_path, meteor_settings_updates)
Expand Down
668 changes: 598 additions & 70 deletions components/log-viewer-webui/client/package-lock.json

Large diffs are not rendered by default.

5 changes: 5 additions & 0 deletions components/log-viewer-webui/client/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,11 @@
]
},
"dependencies": {
"@emotion/react": "^11.13.0",
"@emotion/styled": "^11.13.0",
"@mui/joy": "^5.0.0-beta.48",
"@types/react": "^18.3.3",
"axios": "^1.7.2",
"react": "^18.3.1",
"react-dom": "^18.3.1"
}
Expand Down
5 changes: 5 additions & 0 deletions components/log-viewer-webui/client/public/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,11 @@
<meta charset="utf-8"/>
<meta name="description" content="YScope Log Viewer">
<meta name="viewport" content="initial-scale=1, maximum-scale=1">
<link rel="preconnect" href="https://fonts.googleapis.com"/>
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin/>
<link rel="stylesheet"
href="https://fonts.googleapis.com/css2?family=Inter:wght@300;400;500;600;700&display=swap"
/>
</head>
<body>
<div id="root"></div>
Expand Down
10 changes: 9 additions & 1 deletion components/log-viewer-webui/client/src/App.jsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,19 @@
import {CssVarsProvider} from "@mui/joy/styles/CssVarsProvider";

import LOCAL_STORAGE_KEY from "./typings/LOCAL_STORAGE_KEY.js";
import QueryStatus from "./ui/QueryStatus.jsx";


/**
* Renders the main application.
*
* @return {JSX.Element}
*/
const App = () => {
return (
<h1>Hello world!</h1>
<CssVarsProvider modeStorageKey={LOCAL_STORAGE_KEY.UI_THEME}>
<QueryStatus/>
</CssVarsProvider>
);
};

Expand Down
32 changes: 32 additions & 0 deletions components/log-viewer-webui/client/src/api/query.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import axios from "axios";


/**
* @typedef {object} ExtractIrResp
* @property {number} begin_msg_ix
* @property {number} end_msg_ix
* @property {string} file_split_id
* @property {boolean} is_last_ir_chunk
* @property {string} orig_file_id
* @property {string} path
* @property {string} _id
*/

/**
* Submits a job to extract the split of an original file that contains a given log event. The file
* is extracted as a CLP IR file.
*
* @param {number|string} origFileId The ID of the original file
* @param {number} logEventIdx The index of the log event
* @param {Function} onUploadProgress Callback to handle upload progress events.
* @return {Promise<axios.AxiosResponse<ExtractIrResp>>}
*/
const submitExtractIrJob = async (origFileId, logEventIdx, onUploadProgress) => {
return await axios.post(
"/query/extract-ir",
{logEventIdx, origFileId},
{onUploadProgress}
);
};

export {submitExtractIrJob};
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* Enum of `window.localStorage` keys.
*/
const LOCAL_STORAGE_KEY = Object.freeze({
UI_THEME: "uiTheme",
});

export default LOCAL_STORAGE_KEY;
37 changes: 37 additions & 0 deletions components/log-viewer-webui/client/src/typings/query.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/**
* @typedef {number} QueryLoadingState
*/
let enumQueryLoadingState;
/**
* Enum of query loading state.
*
* @enum {QueryLoadingState}
*/
const QUERY_LOADING_STATES = Object.freeze({
SUBMITTING: (enumQueryLoadingState = 0),
WAITING: ++enumQueryLoadingState,
LOADING: ++enumQueryLoadingState,
});

/**
* Descriptions for query loading states.
*/
const QUERY_LOADING_STATE_DESCRIPTIONS = Object.freeze({
[QUERY_LOADING_STATES.SUBMITTING]: {
label: "Submitting query Job",
description: "Parsing arguments and submitting job to the server.",
},
[QUERY_LOADING_STATES.WAITING]: {
label: "Waiting for job to finish",
description: "The job is running. Waiting for the job to finish.",
},
[QUERY_LOADING_STATES.LOADING]: {
label: "Loading Log Viewer",
description: "The query has been completed and the results are being loaded.",
},
});

export {
QUERY_LOADING_STATE_DESCRIPTIONS,
QUERY_LOADING_STATES,
};
24 changes: 24 additions & 0 deletions components/log-viewer-webui/client/src/ui/Loading.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
.loading-sheet {
height: 100%;

display: flex;
flex-direction: column;

align-items: center;
justify-content: center;
}

.loading-progress-container {
width: 100%;
}

.loading-stepper-container {
display: flex;
flex-grow: 1;

align-items: center;
}

.loading-stepper {
--Stepper-verticalGap: 2rem !important;
}
130 changes: 130 additions & 0 deletions components/log-viewer-webui/client/src/ui/Loading.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
import {
Box,
LinearProgress,
Sheet,
Step,
StepIndicator,
Stepper,
Typography,
} from "@mui/joy";

import {
QUERY_LOADING_STATE_DESCRIPTIONS,
QUERY_LOADING_STATES,
} from "../typings/query.js";

import "./Loading.css";


/**
* Renders a step with a label and description.
*
* @param {object} props
* @param {string} props.description
* @param {boolean} props.isActive
* @param {boolean} props.isError
* @param {string} props.label
* @param {number | string} props.stepIndicatorText
* @return {React.ReactElement}
*/
const LoadingStep = ({
description,
isActive,
isError,
label,
stepIndicatorText,
}) => {
let color = isActive ?
"primary" :
"neutral";

if (isError) {
color = "danger";
}

return (
<Step
indicator={
<StepIndicator
color={color}
variant={isActive ?
"solid" :
"outlined"}
>
{stepIndicatorText}
</StepIndicator>
}
>
<Typography
color={color}
level={"title-lg"}
>
{label}
</Typography>
<Typography level={"body-sm"}>
{description}
</Typography>
</Step>
);
};

/**
* Displays status of a pending query job.
*
* @param {object} props
* @param {QueryLoadState} props.currentState
* @param {string} props.errorMsg
* @return {React.ReactElement}
*/
const Loading = ({currentState, errorMsg}) => {
const steps = [];
Object.values(QUERY_LOADING_STATES).forEach((state) => {
const isActive = (currentState === state);
const stateDescription = QUERY_LOADING_STATE_DESCRIPTIONS[state];
steps.push(
<LoadingStep
description={stateDescription.description}
isActive={isActive}
isError={false}
key={state}
label={stateDescription.label}
stepIndicatorText={state + 1}/>
);
if (isActive && null !== errorMsg) {
steps.push(
<LoadingStep
description={errorMsg}
isActive={isActive}
isError={true}
key={`${state}-error`}
label={"Error"}
stepIndicatorText={"X"}/>
);
}
});

return (
<>
<Sheet className={"loading-sheet"}>
<Box className={"loading-progress-container"}>
<LinearProgress
determinate={null !== errorMsg}
color={null === errorMsg ?
"primary" :
"danger"}/>
</Box>
<Box className={"loading-stepper-container"}>
<Stepper
className={"loading-stepper"}
orientation={"vertical"}
size={"lg"}
>
{steps}
</Stepper>
</Box>
</Sheet>
</>
);
};

export default Loading;
80 changes: 80 additions & 0 deletions components/log-viewer-webui/client/src/ui/QueryStatus.jsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
import {
useEffect,
useRef,
useState,
} from "react";

import {AxiosError} from "axios";

import {submitExtractIrJob} from "../api/query.js";
import {QUERY_LOADING_STATES} from "../typings/query.js";
import Loading from "./Loading.jsx";


/**
* Submits queries and renders the query states.
*
* @return {React.ReactElement}
*/
const QueryStatus = () => {
const [queryState, setQueryState] = useState(QUERY_LOADING_STATES.SUBMITTING);
const [errorMsg, setErrorMsg] = useState(null);
const isFirstRun = useRef(true);

useEffect(() => {
if (false === isFirstRun.current) {
return;
}
isFirstRun.current = false;

const searchParams = new URLSearchParams(window.location.search);
const origFileId = searchParams.get("origFileId");
const logEventIdx = searchParams.get("logEventIdx");
if (null === origFileId || null === logEventIdx) {
const error = "Either `origFileId` or `logEventIdx` are missing from the URL " +
"parameters. Note that non-IR-extraction queries are not supported at the moment.";

console.error(error);
setErrorMsg(error);
return;
}

submitExtractIrJob(
origFileId,
Number(logEventIdx),
() => {
setQueryState(QUERY_LOADING_STATES.WAITING);
}
)
.then(({data}) => {
setQueryState(QUERY_LOADING_STATES.LOADING);

const innerLogEventNum = logEventIdx - data.begin_msg_ix + 1;
window.location = `/log-viewer/index.html?filePath=/ir/${data.path}` +
`#logEventIdx=${innerLogEventNum}`;
})
.catch((e) => {
let msg = "Unknown error.";
if (e instanceof AxiosError) {
msg = e.message;
if ("undefined" !== typeof e.response) {
if ("undefined" !== typeof e.response.data.message) {
msg = e.response.data.message;
} else {
msg = e.response.statusText;
}
}
}
console.error(msg, e);
setErrorMsg(msg);
});
}, []);

return (
<Loading
currentState={queryState}
errorMsg={errorMsg}/>
);
};

export default QueryStatus;
8 changes: 8 additions & 0 deletions components/log-viewer-webui/client/webpack.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,14 @@ const plugins = [
];

const config = {
devServer: {
proxy: [
{
context: ["/"],
target: "http://localhost:3000",
},
],
},
devtool: isProduction ?
"source-map" :
"eval-source-map",
Expand Down
Loading

0 comments on commit e9d5b2b

Please sign in to comment.