Skip to content

Commit

Permalink
Merge pull request #1505 from orchest/feat/auto-save-jupyterlab-config
Browse files Browse the repository at this point in the history
Auto save Jupyterlab setup script
  • Loading branch information
iannbing committed Jan 16, 2023
2 parents e1c35fe + da75922 commit 0de40b4
Show file tree
Hide file tree
Showing 13 changed files with 280 additions and 145 deletions.
2 changes: 1 addition & 1 deletion services/orchest-webserver/client/src/Routes.tsx
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import React from "react";
import { Redirect, Route, Switch, useLocation } from "react-router-dom";
import { ConfigureGitSshView } from "./config-git-ssh-view/ConfigureGitSshView";
import ConfigureJupyterLabView from "./config-jupyterlab-view/ConfigureJupyterLabView";
import { EnvironmentsView } from "./environments-view/EnvironmentsView";
import { JobsView } from "./jobs-view/JobsView";
import { NotificationSettingsView } from "./notification-settings-view/NotificationSettingsView";
import PipelineView from "./pipeline-view/PipelineView";
import { ProjectsView } from "./projects-view/ProjectsView";
import { getOrderedRoutes, siteMap } from "./routingConfig";
import { SettingsView } from "./settings-view/SettingsView";
import ConfigureJupyterLabView from "./views/ConfigureJupyterLabView";
import HelpView from "./views/HelpView";
import JupyterLabView from "./views/JupyterLabView";
import ManageUsersView from "./views/ManageUsersView";
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { fetcher } from "@orchest/lib-utils";

export const JUPYTER_LAB_SETUP_SCRIPT_API_URL = "/async/jupyter-setup-script";

/** Fetches the setup script of JupyterLab */
export const fetchOne = () =>
fetcher<{ script: string }>(JUPYTER_LAB_SETUP_SCRIPT_API_URL).then(
(response) => response.script
);

/** Update JupyterLab setup script */
export const update = (payload: string) => {
const formData = new FormData();
formData.append("setup_script", payload);
return fetcher<void>(JUPYTER_LAB_SETUP_SCRIPT_API_URL, {
method: "POST",
body: formData,
});
};

export const jupyterLabSetupScriptApi = {
fetchOne,
update,
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import create from "zustand";
import { jupyterLabSetupScriptApi } from "./jupyterLabSetupScriptApi";

export type JupyterLabSetupScriptApi = {
setupScript: string | undefined;
fetch: () => Promise<string>;
update: (payload: string) => Promise<void>;
};

export const useJupyterLabSetupScriptApi = create<JupyterLabSetupScriptApi>(
(set) => {
return {
setupScript: undefined,
fetch: async () => {
const setupScript = await jupyterLabSetupScriptApi.fetchOne();
set({ setupScript });
return setupScript;
},
update: async (payload) => {
set({ setupScript: payload });
return await jupyterLabSetupScriptApi.update(payload);
},
};
}
);
Original file line number Diff line number Diff line change
Expand Up @@ -23,14 +23,14 @@ export const GitConfigAttribute = ({
}: GitConfigAttributeProps) => {
const {
value,
setValue,
handleChange,
isValid,
isDirty,
initValue,
setAsDirtyOnBlur: handleBlur,
} = useTextField(GIT_CONFIG_KEYS[name]);

useInitGitConfigAttribute(name, setValue);
useInitGitConfigAttribute(name, initValue);
const setConfig = useGitConfigsApi((state) => state.setConfig);
React.useEffect(() => {
setConfig((config) => ({ ...config, [name]: value }));
Expand Down Expand Up @@ -59,7 +59,7 @@ export const GitConfigAttribute = ({
/** Initialize the attribute when config is just loaded in the store. */
const useInitGitConfigAttribute = (
name: keyof Omit<GitConfig, "uuid">,
setValue: React.Dispatch<React.SetStateAction<string>>
initValue: (value: string) => void
) => {
const initialConfig = useGitConfigsApi(
(state) => state.config,
Expand All @@ -69,8 +69,8 @@ const useInitGitConfigAttribute = (
}
);

const initialValue = initialConfig?.[name];
const value = initialConfig?.[name];
React.useEffect(() => {
if (initialValue) setValue(initialValue);
}, [setValue, initialValue]);
if (value) initValue(value);
}, [initValue, value]);
};
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
import { useJupyterLabSetupScriptApi } from "@/api/jupyter-lab-setup-script/useJupyterLabSetupScriptApi";
import { useOrchestConfigsApi } from "@/api/system-config/useOrchestConfigsApi";
import { Code } from "@/components/common/Code";
import { SnackBar } from "@/components/common/SnackBar";
Expand All @@ -12,26 +13,19 @@ import { SettingsViewLayout } from "@/settings-view/SettingsViewLayout";
import { JupyterImageBuild } from "@/types";
import CloseIcon from "@mui/icons-material/Close";
import MemoryIcon from "@mui/icons-material/Memory";
import SaveIcon from "@mui/icons-material/Save";
import Button from "@mui/material/Button";
import LinearProgress from "@mui/material/LinearProgress";
import Stack from "@mui/material/Stack";
import Typography from "@mui/material/Typography";
import { hasValue, uuidv4 } from "@orchest/lib-utils";
import "codemirror/mode/shell/shell";
import React from "react";
import { Controlled as CodeMirror } from "react-codemirror2";
import { JupyterLabSetupScript } from "./JupyterLabSetupScript";

const CANCELABLE_STATUSES = ["PENDING", "STARTED"];

const ConfigureJupyterLabView: React.FC = () => {
// global
const {
state: { hasUnsavedChanges },
setAlert,
setConfirm,
setAsSaved,
} = useGlobalContext();
const { setAlert, setConfirm } = useGlobalContext();
const config = useOrchestConfigsApi((state) => state.config);
const {
deleteAllSessions,
Expand All @@ -51,9 +45,6 @@ const ConfigureJupyterLabView: React.FC = () => {

const [isBuildingImage, setIsBuildingImage] = React.useState(false);
const [isCancellingBuild, setIsCancellingBuild] = React.useState(false);
const [jupyterSetupScript, setJupyterSetupScript] = React.useState<
string | undefined
>(undefined);

const [
hasStartedKillingSessions,
Expand All @@ -68,24 +59,6 @@ const ConfigureJupyterLabView: React.FC = () => {
: false;
}, [jupyterBuild]);

const save = React.useCallback(async () => {
if (!hasValue(jupyterSetupScript)) return;

let formData = new FormData();
formData.append("setup_script", jupyterSetupScript);

try {
await cancelableFetch("/async/jupyter-setup-script", {
method: "POST",
body: formData,
});
setAsSaved();
} catch (e) {
setAsSaved(false);
console.error(e);
}
}, [jupyterSetupScript, setAsSaved, cancelableFetch]);

const {
dispatch,
state: { pipelineReadOnlyReason },
Expand All @@ -108,7 +81,6 @@ const ConfigureJupyterLabView: React.FC = () => {
setIgnoreIncomingLogs(true);

try {
await save();
const response = await cancelableFetch<{
jupyter_image_build: JupyterImageBuild;
}>("/catch/api-proxy/api/jupyter-builds", { method: "POST" });
Expand Down Expand Up @@ -149,7 +121,7 @@ const ConfigureJupyterLabView: React.FC = () => {
}
}
setIsBuildingImage(false);
}, [deleteAllSessions, save, setAlert, setConfirm, cancelableFetch]);
}, [deleteAllSessions, setAlert, setConfirm, cancelableFetch]);

const cancelImageBuild = async () => {
// send DELETE to cancel ongoing build
Expand Down Expand Up @@ -182,21 +154,6 @@ const ConfigureJupyterLabView: React.FC = () => {
}
};

const getSetupScript = React.useCallback(async () => {
try {
const { script } = await cancelableFetch<{ script: string }>(
"/async/jupyter-setup-script"
);
setJupyterSetupScript(script || "");
} catch (e) {
setAlert("Error", `Failed to fetch setup script. ${e}`);
}
}, [setAlert, cancelableFetch]);

React.useEffect(() => {
getSetupScript();
}, [getSetupScript]);

React.useEffect(() => {
const isAllSessionsDeletedForBuildingImage =
hasStartedKillingSessions && // attempted to build image but got stuck, so started to kill sessions
Expand All @@ -215,6 +172,10 @@ const ConfigureJupyterLabView: React.FC = () => {
buildImage,
]);

const isLoading = useJupyterLabSetupScriptApi(
(state) => !hasValue(state.setupScript)
);

const showStoppingAllSessionsWarning =
hasStartedKillingSessions && // attempted to build image but got stuck, so started to kill sessions
!sessionsKillAllInProgress && // the operation of deleting sessions is done
Expand All @@ -224,103 +185,85 @@ const ConfigureJupyterLabView: React.FC = () => {
return (
<SettingsViewLayout
header={
<Typography variant="h5" flex={1}>
<Typography
variant="h5"
flex={1}
sx={{ paddingBottom: (theme) => theme.spacing(2) }}
>
Configure JupyterLab
</Typography>
}
>
<Stack sx={{ marginTop: (theme) => theme.spacing(2) }}>
{hasValue(jupyterSetupScript) ? (
<>
<p className="push-down">
You can install JupyterLab extensions using the bash script below.
</p>
<p className="push-down">
For example, you can install the Jupyterlab Code Formatter
extension by executing{" "}
<Code>pip install jupyterlab_code_formatter</Code>.
</p>
<Stack sx={{ margin: (theme) => theme.spacing(2, 0, 4) }}>
<>
<p className="push-down">
You can install JupyterLab extensions using the bash script below.
</p>
<p className="push-down">
{`For example, you can install the Jupyterlab Code Formatter
extension by executing `}
<Code>pip install jupyterlab_code_formatter</Code>.
</p>

<p className="push-down">
In addition, you can configure the JupyterLab environment to
include settings such as your <Code>git</Code> username and email.
<br />
<br />
<Code>{`git config --global user.name "John Doe"`}</Code>
<br />
<Code>{`git config --global user.email "john@example.org"`}</Code>
</p>
<p className="push-down">
In addition, you can configure the JupyterLab environment to include
settings such as your <Code>git</Code> username and email.
<br />
<br />
<Code>{`git config --global user.name "John Doe"`}</Code>
<br />
<Code>{`git config --global user.email "john@example.org"`}</Code>
</p>

<div className="push-down">
<CodeMirror
value={jupyterSetupScript}
options={{
mode: "application/x-sh",
theme: "dracula",
lineNumbers: true,
viewportMargin: Infinity,
readOnly: building,
}}
onBeforeChange={(editor, data, value) => {
setJupyterSetupScript(value);
setAsSaved(false);
}}
/>
</div>
<div className="push-down">
<JupyterLabSetupScript readOnly={building} />
</div>

<LegacyImageBuildLog
buildRequestEndpoint={
"/catch/api-proxy/api/jupyter-builds/most-recent"
}
buildsKey="jupyter_image_builds"
socketIONamespace={
config?.ORCHEST_SOCKETIO_JUPYTER_IMG_BUILDING_NAMESPACE
}
streamIdentity={"jupyter"}
onUpdateBuild={setJupyterEnvironmentBuild}
ignoreIncomingLogs={ignoreIncomingLogs}
build={jupyterBuild}
buildFetchHash={buildFetchHash}
/>
<LegacyImageBuildLog
buildRequestEndpoint={
"/catch/api-proxy/api/jupyter-builds/most-recent"
}
buildsKey="jupyter_image_builds"
socketIONamespace={
config?.ORCHEST_SOCKETIO_JUPYTER_IMG_BUILDING_NAMESPACE
}
streamIdentity={"jupyter"}
onUpdateBuild={setJupyterEnvironmentBuild}
ignoreIncomingLogs={ignoreIncomingLogs}
build={jupyterBuild}
buildFetchHash={buildFetchHash}
/>

<Stack
sx={{ marginTop: (theme) => theme.spacing(2) }}
direction="row"
spacing={2}
>
<Stack
sx={{ marginTop: (theme) => theme.spacing(2) }}
direction="row"
spacing={2}
>
{!building ? (
<Button
disabled={
isLoading || isBuildingImage || hasStartedKillingSessions
}
startIcon={<MemoryIcon />}
color="secondary"
variant="contained"
onClick={buildImage}
>
Build
</Button>
) : (
<Button
startIcon={<SaveIcon />}
disabled={isCancellingBuild}
startIcon={<CloseIcon />}
color="secondary"
variant="contained"
onClick={save}
onClick={cancelImageBuild}
>
{hasUnsavedChanges ? "Save*" : "Save"}
Cancel build
</Button>
{!building ? (
<Button
disabled={isBuildingImage || hasStartedKillingSessions}
startIcon={<MemoryIcon />}
color="secondary"
variant="contained"
onClick={buildImage}
>
Build
</Button>
) : (
<Button
disabled={isCancellingBuild}
startIcon={<CloseIcon />}
color="secondary"
variant="contained"
onClick={cancelImageBuild}
>
Cancel build
</Button>
)}
</Stack>
</>
) : (
<LinearProgress />
)}
)}
</Stack>
</>
</Stack>
<SnackBar
open={showStoppingAllSessionsWarning}
Expand Down

0 comments on commit 0de40b4

Please sign in to comment.