Skip to content

Commit

Permalink
feat: package installation from pypi (#961)
Browse files Browse the repository at this point in the history
  • Loading branch information
akshayka authored Mar 20, 2024
1 parent 8667c05 commit d9fbeb8
Show file tree
Hide file tree
Showing 31 changed files with 1,600 additions and 9 deletions.
251 changes: 251 additions & 0 deletions frontend/src/components/editor/package-alert.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,251 @@
/* Copyright 2024 Marimo. All rights reserved. */

import { cn } from "@/utils/cn";
import { RuntimeState } from "@/core/kernel/RuntimeState";
import { sendInstallMissingPackages } from "@/core/network/requests";
import {
useAlerts,
useAlertActions,
MissingPackageAlert,
InstallingPackageAlert,
isMissingPackageAlert,
isInstallingPackageAlert,
} from "@/core/alerts/state";
import { Banner } from "@/plugins/impl/common/error-banner";
import {
PackageXIcon,
BoxIcon,
CheckIcon,
DownloadCloudIcon,
PackageCheckIcon,
XIcon,
} from "lucide-react";
import React from "react";
import { Button } from "../ui/button";
import { PackageInstallationStatus } from "@/core/kernel/messages";
import { logNever } from "@/utils/assertNever";

export const PackageAlert: React.FC = (props) => {
const { packageAlert } = useAlerts();
const { addPackageAlert, clearPackageAlert } = useAlertActions();

if (packageAlert === null) {
return null;
}

if (isMissingPackageAlert(packageAlert)) {
return (
<div className="flex flex-col gap-4 mb-5 fixed top-5 left-5 w-[400px] z-[200] opacity-95">
<Banner
kind="danger"
className="flex flex-col rounded py-3 px-5 animate-in slide-in-from-left"
>
<div className="flex justify-between">
<span className="font-bold text-lg flex items-center mb-2">
<PackageXIcon className="w-5 h-5 inline-block mr-2" />
Missing packages
</span>
<Button
variant="text"
size="icon"
onClick={() => clearPackageAlert(packageAlert.id)}
>
<XIcon className="w-5 h-5" />
</Button>
</div>
<div className="flex flex-col gap-4 justify-between items-start text-muted-foreground text-base">
<div>
<p>The following packages were not found:</p>
<ul className="list-disc ml-4 mt-1">
{packageAlert.packages.map((pkg, index) => (
<li
className="flex items-center gap-1 font-mono text-sm"
key={index}
>
<BoxIcon size="1rem" />
{pkg}
</li>
))}
</ul>
</div>
<div className="ml-auto">
{packageAlert.isolated ? (
<InstallPackagesButton
packages={packageAlert.packages}
addPackageAlert={addPackageAlert}
/>
) : (
<p>
If you set up a{" "}
<a
href="https://docs.python.org/3/tutorial/venv.html#creating-virtual-environments"
className="text-accent-foreground hover:underline"
target="_blank"
rel="noreferrer"
>
virtual environment
</a>
, marimo can install these packages for you.
</p>
)}
</div>
</div>
</Banner>
</div>
);
} else if (isInstallingPackageAlert(packageAlert)) {
const { status, title, titleIcon, description } =
getInstallationStatusElements(packageAlert.packages);
if (status === "installed") {
setTimeout(() => clearPackageAlert(packageAlert.id), 10_000);
}

return (
<div className="flex flex-col gap-4 mb-5 fixed top-5 left-5 w-[400px] z-[200] opacity-95">
<Banner
kind={status === "failed" ? "danger" : "info"}
className="flex flex-col rounded pt-3 pb-4 px-5"
>
<div className="flex justify-between">
<span className="font-bold text-lg flex items-center mb-2">
{titleIcon}
{title}
</span>
<Button
variant="text"
size="icon"
onClick={() => clearPackageAlert(packageAlert.id)}
>
<XIcon className="w-5 h-5" />
</Button>
</div>
<div
className={cn(
"flex flex-col gap-4 justify-between items-start text-muted-foreground text-base",
status === "installed" && "text-accent-foreground",
)}
>
<div>
<p>{description}</p>
<ul className="list-disc ml-4 mt-1">
{Object.entries(packageAlert.packages).map(
([pkg, st], index) => (
<li
className={cn(
"flex items-center gap-1 font-mono text-sm",
st === "installing" && "font-semibold",
st === "failed" && "text-destructive",
st === "installed" && "text-accent-foreground",
st === "installed" &&
status === "failed" &&
"text-muted-foreground",
)}
key={index}
>
<ProgressIcon status={st} />
{pkg}
</li>
),
)}
</ul>
</div>
</div>
</Banner>
</div>
);
} else {
logNever(packageAlert);
return null;
}
};

function getInstallationStatusElements(packages: PackageInstallationStatus) {
const statuses = new Set(Object.values(packages));
const status =
statuses.has("queued") || statuses.has("installing")
? "installing"
: statuses.has("failed")
? "failed"
: "installed";

if (status === "installing") {
return {
status: "installing",
title: "Installing packages",
titleIcon: <DownloadCloudIcon className="w-5 h-5 inline-block mr-2" />,
description: "Installing packages:",
};
} else if (status === "installed") {
return {
status: "installed",
title: "All packages installed!",
titleIcon: <PackageCheckIcon className="w-5 h-5 inline-block mr-2" />,
description: "Installed packages:",
};
} else {
return {
status: "failed",
title: "Some packages failed to install",
titleIcon: <PackageXIcon className="w-5 h-5 inline-block mr-2" />,
description: "See terminal for error logs.",
};
}
}

const ProgressIcon = ({
status,
}: {
status: PackageInstallationStatus[string];
}) => {
switch (status) {
case "queued":
return <BoxIcon size="1rem" />;
case "installing":
return <DownloadCloudIcon size="1rem" />;
case "installed":
return <CheckIcon size="1rem" />;
case "failed":
return <XIcon size="1rem" />;
default:
logNever(status);
return null;
}
};

async function installPackages(
packages: string[],
addPackageAlert: (
alert: MissingPackageAlert | InstallingPackageAlert,
) => void,
) {
const packageStatus = Object.fromEntries(
packages.map((pkg) => [pkg, "queued"]),
) as PackageInstallationStatus;
addPackageAlert({
kind: "installing",
packages: packageStatus,
});
RuntimeState.INSTANCE.registerRunStart();
await sendInstallMissingPackages({ manager: "pip" });
}

const InstallPackagesButton = ({
packages,
addPackageAlert,
}: {
packages: string[];
addPackageAlert: (
alert: MissingPackageAlert | InstallingPackageAlert,
) => void;
}) => {
return (
<Button
variant="outline"
size="sm"
onClick={() => installPackages(packages, addPackageAlert)}
>
<DownloadCloudIcon className="w-4 h-4 mr-2" />
<span className="font-semibold">Install with pip</span>
</Button>
);
};
2 changes: 2 additions & 0 deletions frontend/src/components/editor/renderers/CellArray.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import { useDelayVisibility } from "./vertical-layout/useDelayVisibility";
import { useChromeActions } from "../chrome/state";
import { Functions } from "@/utils/functions";
import { NotebookBanner } from "../notebook-banner";
import { PackageAlert } from "@/components/editor/package-alert";

interface CellArrayProps {
notebook: NotebookState;
Expand Down Expand Up @@ -97,6 +98,7 @@ export const CellArray: React.FC<CellArrayProps> = ({
invisible={invisible}
appConfig={appConfig}
>
<PackageAlert />
<NotebookBanner />
{cells.map((cell) => (
<Cell
Expand Down
6 changes: 3 additions & 3 deletions frontend/src/components/ui/button.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -46,9 +46,9 @@ const buttonVariants = cva(
activeCommon,
),
outline: cn(
"border border-slate-500 shadow-smSolid",
"hover:bg-secondary/90 hover:text-secondary-foreground",
"hover:border-input",
"border border-slate-300 shadow-smSolid",
"hover:bg-accent hover:text-accent-foreground",
"hover:border-primary",
activeCommon,
),
secondary: cn(
Expand Down
86 changes: 86 additions & 0 deletions frontend/src/core/alerts/state.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
/* Copyright 2024 Marimo. All rights reserved. */

import { PackageInstallationStatus } from "@/core/kernel/messages";
import { createReducer } from "@/utils/createReducer";
import { generateUUID } from "@/utils/uuid";
import { atom, useAtomValue, useSetAtom } from "jotai";
import { useMemo } from "react";

type Identified<T> = { id: string } & T;

export interface MissingPackageAlert {
kind: "missing";
packages: string[];
isolated: boolean;
}

export interface InstallingPackageAlert {
kind: "installing";
packages: PackageInstallationStatus;
}

export function isMissingPackageAlert(
alert: MissingPackageAlert | InstallingPackageAlert,
): alert is MissingPackageAlert {
return alert.kind === "missing";
}

export function isInstallingPackageAlert(
alert: MissingPackageAlert | InstallingPackageAlert,
): alert is InstallingPackageAlert {
return alert.kind === "installing";
}

/** Prominent alerts.
*
* Right now we only have one type of alert.
*/
interface AlertState {
packageAlert:
| Identified<MissingPackageAlert>
| Identified<InstallingPackageAlert>
| null;
}

const { reducer, createActions } = createReducer(
() => ({ packageAlert: null }) as AlertState,
{
addPackageAlert: (
state,
alert: MissingPackageAlert | InstallingPackageAlert,
) => {
return {
...state,
packageAlert: { id: generateUUID(), ...alert },
};
},

clearPackageAlert: (state, id: string) => {
return state.packageAlert !== null && state.packageAlert.id === id
? { ...state, packageAlert: null }
: state;
},
},
);

const alertAtom = atom<AlertState>({
packageAlert: null,
});

/**
* React hook to get the Alert state.
*/
export const useAlerts = () => useAtomValue(alertAtom);

/**
* React hook to get the Alerts actions.
*/
export function useAlertActions() {
const setState = useSetAtom(alertAtom);
return useMemo(() => {
const actions = createActions((action) => {
setState((state) => reducer(state, action));
});
return actions;
}, [setState]);
}
Loading

0 comments on commit d9fbeb8

Please sign in to comment.