Skip to content

Commit

Permalink
feat: add app-chrome and variables to editor (#110)
Browse files Browse the repository at this point in the history
* add variables table to storybook

* udpates

* wip

* updates

* add hotekey, other fixes

* lint, test

* format marimo variable values

* dont send variable values if there are no variables

* update UI element values

* styling

* add todo for state

* nit consistency

* sidebar init fix

* focus

* remove cookies

* add shadow color

* lint

* minor tweaks

- fix app status indicator
- badge: green -> outline (less distracting)
- cell focus outline: blue, solid instead of green fuzzy
- remove number of variables from variable button (looked like errors?)

* fix test

---------

Co-authored-by: Akshay Agrawal <akshay@marimo.io>
  • Loading branch information
mscolnick and akshayka committed Sep 20, 2023
1 parent 5b00b88 commit db77ab8
Show file tree
Hide file tree
Showing 32 changed files with 826 additions and 192 deletions.
78 changes: 44 additions & 34 deletions frontend/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -288,47 +288,55 @@ export const App: React.FC<AppProps> = ({ userConfig, appConfig }) => {
/>
);

return (
<div
id="App"
className={clsx(
connStatus.state === WebSocketState.CLOSED && "disconnected",
"bg-background w-full h-full text-textColor",
"flex flex-col",
appConfig.width === "full" && "config-width-full"
)}
>
const statusOverlay = (
<>
{connStatus.state === WebSocketState.OPEN && isRunning && <RunningIcon />}
{connStatus.state === WebSocketState.CLOSED && <NoiseBackground />}
{connStatus.state === WebSocketState.CLOSED && <DisconnectedIcon />}
</>
);

return (
<>
{statusOverlay}
<div
id="App"
className={clsx(
(isEditing || isPresenting) && "pt-4 sm:pt-12 pb-2 mb-4",
(isPresenting || isReading) && "sm:pt-8"
connStatus.state === WebSocketState.CLOSED && "disconnected",
"bg-background w-full h-full text-textColor",
"flex flex-col overflow-y-auto overflow-x-hidden",
appConfig.width === "full" && "config-width-full"
)}
>
{isEditing && (
<div id="Welcome">
<FilenameForm
filename={filename}
setFilename={handleFilenameChange}
/>
</div>
)}
{connStatus.state === WebSocketState.CLOSED && (
<Disconnected reason={connStatus.reason} />
<div
className={clsx(
(isEditing || isPresenting) && "pt-4 sm:pt-12 pb-2 mb-4",
(isPresenting || isReading) && "sm:pt-8"
)}
>
{isEditing && (
<div id="Welcome">
<FilenameForm
filename={filename}
setFilename={handleFilenameChange}
/>
</div>
)}
{connStatus.state === WebSocketState.CLOSED && (
<Disconnected reason={connStatus.reason} />
)}
</div>

{/* Don't render until we have a single cell */}
{cells.present.length > 0 && (
<CellsRenderer appConfig={appConfig} mode={viewState.mode}>
<SortableCellsProvider disabled={!isEditing}>
{editableCellsArray}
</SortableCellsProvider>
</CellsRenderer>
)}
</div>

{/* Don't render until we have a single cell */}
{cells.present.length > 0 && (
<CellsRenderer appConfig={appConfig} mode={viewState.mode}>
<SortableCellsProvider disabled={!isEditing}>
{editableCellsArray}
</SortableCellsProvider>
</CellsRenderer>
)}

{(isEditing || isPresenting) && (
<Controls
filename={filename}
Expand All @@ -348,21 +356,23 @@ export const App: React.FC<AppProps> = ({ userConfig, appConfig }) => {
undoAvailable={cells.history.length > 0}
/>
)}
</div>
</>
);
};

const topLeftStatus =
"absolute top-3 left-4 m-0 flex items-center space-x-3 min-h-[28px] no-print pointer-events-auto z-30";
const DisconnectedIcon = () => (
<Tooltip content="App disconnected">
<div className="app-status-indicator">
<div className={topLeftStatus}>
<UnlinkIcon className="closed-app-icon" />
</div>
</Tooltip>
);

const RunningIcon = () => (
<div
className="app-status-indicator"
className={topLeftStatus}
title={"Marimo is busy computing. Hang tight!"}
>
<HourglassIcon className="running-app-icon" size={30} strokeWidth={1} />
Expand Down
21 changes: 16 additions & 5 deletions frontend/src/MarimoApp.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ import { DayPickerProvider } from "react-day-picker";
import { CommandPallette } from "./editor/CommandPallette";
import { useAppConfig, useUserConfig } from "@/core/state/config";
import { initialMode } from "./core/mode";
import { AppChrome } from "./editor/chrome/wrapper/app-chrome";

/**
* The root component of the Marimo app.
Expand All @@ -23,15 +24,25 @@ export const MarimoApp: React.FC = () => {
initializePlugins();
}, []);

const body =
initialMode === "read" ? (
<>
<App userConfig={userConfig} appConfig={appConfig} />
<Toaster />
</>
) : (
<AppChrome>
<App userConfig={userConfig} appConfig={appConfig} />
<Toaster />
<CommandPallette />
</AppChrome>
);

return (
<ErrorBoundary>
<TooltipProvider>
<DayPickerProvider initialProps={{}}>
<ModalProvider>
<App userConfig={userConfig} appConfig={appConfig} />
<Toaster />
{initialMode !== "read" && <CommandPallette />}
</ModalProvider>
<ModalProvider>{body}</ModalProvider>
</DayPickerProvider>
</TooltipProvider>
</ErrorBoundary>
Expand Down
6 changes: 4 additions & 2 deletions frontend/src/components/ui/badge.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ import { VariantProps, cva } from "class-variance-authority";
import { cn } from "@/lib/utils";

const badgeVariants = cva(
"inline-flex items-center border rounded-full px-2.5 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
"inline-flex items-center border rounded-full px-2 py-0.5 text-xs font-semibold transition-colors focus:outline-none focus:ring-2 focus:ring-ring focus:ring-offset-2",
{
variants: {
variant: {
Expand All @@ -14,7 +14,9 @@ const badgeVariants = cva(
secondary:
"bg-secondary hover:bg-secondary/80 border-transparent text-secondary-foreground",
destructive:
"bg-destructive hover:bg-destructive/80 border-transparent text-destructive-foreground",
"bg-[var(--red-2)] border-[var(--red-6)] text-[var(--red-9)] hover:bg-[var(--red-3)]",
success:
"bg-[var(--grass-2)] border-[var(--grass-5)] text-[var(--grass-9)] hover:bg-[var(--grass-3)]",
outline: "text-foreground",
},
},
Expand Down
4 changes: 2 additions & 2 deletions frontend/src/components/ui/table.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ const Table = React.forwardRef<
HTMLTableElement,
React.HTMLAttributes<HTMLTableElement>
>(({ className, ...props }, ref) => (
<div className="w-full overflow-auto">
<div className="w-full overflow-auto flex-1">
<table
ref={ref}
className={cn("w-full caption-bottom text-sm", className)}
Expand Down Expand Up @@ -85,7 +85,7 @@ const TableCell = React.forwardRef<
>(({ className, ...props }, ref) => (
<td
ref={ref}
className={cn("p-4 align-middle [&:has([role=checkbox])]:pr-0", className)}
className={cn("p-3 align-middle [&:has([role=checkbox])]:pr-0", className)}
{...props}
/>
));
Expand Down
153 changes: 153 additions & 0 deletions frontend/src/components/variables/variables-table.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
/* Copyright 2023 Marimo. All rights reserved. */
import React, { memo } from "react";
import {
TableHeader,
TableRow,
TableHead,
TableBody,
TableCell,
Table,
} from "../ui/table";
import { Variables } from "@/core/variables/types";
import { CellId } from "@/core/model/ids";
import { CellLink } from "@/editor/links/cell-link";
import { cn } from "@/lib/utils";
import { SquareEqualIcon, WorkflowIcon } from "lucide-react";
import { Badge } from "../ui/badge";
import { toast } from "../ui/use-toast";

interface Props {
className?: string;
/**
* Used to sort the variables.
*/
cellIds: CellId[];
variables: Variables;
}

export const VariableTable: React.FC<Props> = memo(
({ className, cellIds, variables }) => {
const cellIdToIndex = new Map<CellId, number>();
cellIds.forEach((id, index) => cellIdToIndex.set(id, index));

const sortedVariables = Object.values(variables).sort((a, b) => {
const aIndex = cellIdToIndex.get(a.declaredBy[0]);
const bIndex = cellIdToIndex.get(b.declaredBy[0]);
if (aIndex === undefined || bIndex === undefined) {
return 0;
}
return aIndex - bIndex;
});

return (
<Table className={cn("w-full overflow-hidden text-sm flex-1", className)}>
<TableHeader>
<TableRow className="whitespace-nowrap text-xs">
<TableHead>Name</TableHead>
<TableHead>
<div className="flex flex-col gap-1">
<span>Type</span>
<span>Value</span>
</div>
</TableHead>
<TableHead>
<div className="flex flex-col gap-1">
<span>Declared By</span>
<span>Used By</span>
</div>
</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{sortedVariables.map((variable) => (
<TableRow key={variable.name}>
<TableCell
className="font-medium max-w-[130px]"
title={variable.name}
>
<div>
<Badge
variant={
variable.declaredBy.length > 1 ? "destructive" : "outline"
}
className="rounded-sm text-ellipsis block overflow-hidden max-w-fit cursor-pointer"
onClick={() => {
navigator.clipboard.writeText(variable.name);
toast({ title: "Copied to clipboard" });
}}
>
{variable.name}
</Badge>
</div>
</TableCell>
<TableCell className="max-w-[150px]">
<div className="text-ellipsis overflow-hidden whitespace-nowrap text-muted-foreground font-mono text-xs">
{variable.dataType}
</div>
<div
className="text-ellipsis overflow-hidden whitespace-nowrap"
title={variable.value}
>
{variable.value}
</div>
</TableCell>
<TableCell className="py-1">
<div className="flex flex-col gap-1">
<div className="flex flex-row overflow-auto gap-2 items-center">
<span title="Declared by">
<SquareEqualIcon className="w-3.5 h-3.5 text-muted-foreground" />
</span>

{variable.declaredBy.length === 1 ? (
<CellLink
variant="focus"
cellId={variable.declaredBy[0]}
/>
) : (
<div className="text-destructive flex flex-row gap-2">
{variable.declaredBy.slice(0, 3).map((cellId, idx) => (
<span className="flex" key={cellId}>
<CellLink
variant="focus"
key={cellId}
cellId={cellId}
className="whitespace-nowrap text-destructive"
/>
{idx < variable.declaredBy.length - 1 && ", "}
</span>
))}
</div>
)}
</div>
<div className="flex flex-row overflow-auto gap-2 items-baseline">
<span title="Used by">
<WorkflowIcon className="w-3.5 h-3.5 text-muted-foreground" />
</span>

{variable.usedBy.slice(0, 3).map((cellId, idx) => (
<span className="flex" key={cellId}>
<CellLink
variant="focus"
key={cellId}
cellId={cellId}
className="whitespace-nowrap"
/>
{idx < variable.usedBy.length - 1 && ", "}
</span>
))}
{variable.usedBy.length > 3 && (
<div className="whitespace-nowrap text-muted-foreground text-xs">
+{variable.usedBy.length - 3} more
</div>
)}
</div>
</div>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
);
}
);
VariableTable.displayName = "VariableTable";
5 changes: 5 additions & 0 deletions frontend/src/core/hotkeys/hotkeys.ts
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,11 @@ const DEFAULT_HOT_KEY = {
group: "Navigation",
key: "Mod-Shift-g",
},
"global.toggleSidebar": {
name: "Toggle helper panel",
group: "Navigation",
key: "Mod-Shift-s",
},
} satisfies Record<string, Hotkey>;

export type HotkeyAction = keyof typeof DEFAULT_HOT_KEY;
Expand Down
21 changes: 21 additions & 0 deletions frontend/src/core/kernel/messages.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import { LayoutType } from "@/editor/renderers/types";
import { CellConfig, CellStatus } from "../model/cells";
import { CellId } from "../model/ids";
import { VariableName } from "../variables/types";

export type OutputChannel =
| "output"
Expand Down Expand Up @@ -168,4 +169,24 @@ export type OperationMessage =
| {
op: "cell-op";
data: CellMessage;
}
| {
op: "variables";
data: {
variables: Array<{
name: VariableName;
declared_by: CellId[];
used_by: CellId[];
}>;
};
}
| {
op: "variable-values";
data: {
variables: Array<{
name: VariableName;
datatype?: string;
value?: string;
}>;
};
};
Loading

1 comment on commit db77ab8

@vercel
Copy link

@vercel vercel bot commented on db77ab8 Sep 20, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Successfully deployed to the following URLs:

marimo-storybook – ./frontend

marimo-storybook-git-main-marimo.vercel.app
marimo-storybook-marimo.vercel.app
marimo-storybook.vercel.app

Please sign in to comment.