Skip to content

Commit

Permalink
feat: mo.routes() (marimo-team#1356)
Browse files Browse the repository at this point in the history
* feat: mo.routes()

* lint

* lazy/catch-all improvements

* fixes

* lint

* fix test
  • Loading branch information
mscolnick authored May 22, 2024
1 parent 9a5472f commit b71e1d9
Show file tree
Hide file tree
Showing 21 changed files with 574 additions and 15 deletions.
1 change: 0 additions & 1 deletion .github/PULL_REQUEST_TEMPLATE.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ Provide a concise summary of what this pull request is addressing.
If this PR fixes any issues, list them here by number (e.g., Fixes #123).
-->


## 🔍 Description of Changes

<!--
Expand Down
2 changes: 1 addition & 1 deletion .pre-commit-config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ repos:
hooks:
- id: markdownlint-fix
args: [-c, configs/.markdownlint.yaml, --fix]
exclude: marimo/_tutorials/.*\.md
exclude: ^marimo/_tutorials/.*\.md

- repo: https://github.com/crate-ci/typos
rev: v1.21.0
Expand Down
2 changes: 2 additions & 0 deletions configs/.markdownlint.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -18,3 +18,5 @@ MD001: false
MD040: false
# ol-prefix
MD029: false
# no-trailing-punctuation
MD026: false
2 changes: 2 additions & 0 deletions docs/api/layouts/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
justify
lazy
nav_menu
routes
sidebar
stacks
tree
Expand All @@ -37,6 +38,7 @@ them but just render their children in a certain way.
marimo.left
marimo.nav_menu
marimo.right
marimo.routes
marimo.sidebar
marimo.tree
marimo.vstack
Expand Down
41 changes: 41 additions & 0 deletions docs/api/layouts/routes.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
# Routes

```{eval-rst}
.. marimo-embed::
:size: medium
@app.cell
def __():
mo.sidebar(
[
mo.md("# marimo"),
mo.nav_menu(
{
"#/": f"{mo.icon('lucide:home')} Home",
"#/about": f"{mo.icon('lucide:user')} About",
"#/contact": f"{mo.icon('lucide:phone')} Contact",
"Links": {
"https://twitter.com/marimo_io": "Twitter",
"https://github.com/marimo-team/marimo": "GitHub",
},
},
orientation="vertical",
),
]
)
return
@app.cell
def __():
mo.routes({
"#/": mo.md("# Home"),
"#/about": mo.md("# About"),
"#/contact": mo.md("# Contact"),
mo.routes.CATCH_ALL: mo.md("# Home"),
})
return
```

```{eval-rst}
.. autofunction:: marimo.routes
```
6 changes: 3 additions & 3 deletions docs/api/layouts/sidebar.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@
mo.md("# marimo"),
mo.nav_menu(
{
"#home": f"{mo.icon('lucide:home')} Home",
"#about": f"{mo.icon('lucide:user')} About",
"#contact": f"{mo.icon('lucide:phone')} Contact",
"#/home": f"{mo.icon('lucide:home')} Home",
"#/about": f"{mo.icon('lucide:user')} About",
"#/contact": f"{mo.icon('lucide:phone')} Contact",
"Links": {
"https://twitter.com/marimo_io": "Twitter",
"https://github.com/marimo-team/marimo": "GitHub",
Expand Down
3 changes: 1 addition & 2 deletions docs/guides/deploying/deploying_marimo_cloud.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ Marimo Cloud is currently in beta: if you'd like access, email us at
[[email protected]](mailto:[email protected]) or book time on [our
calendar](calendly.com/akshay-marimo) and we'll onboard you very quickly.


## Marimo Cloud

Marimo Cloud seamlessly augments local development on marimo notebooks with
Expand All @@ -23,7 +22,7 @@ Today, marimo cloud has two main features:

We have many more features planned for the near future, and **big ideas** on
how Marimo Cloud can supercharge the entire lifecycle of working with and
experimenting on data, from small workloads to very large ones as well.
experimenting on data, from small workloads to very large ones as well.

## Reach out to us!

Expand Down
2 changes: 1 addition & 1 deletion docs/guides/deploying/deploying_ploomber.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@

For production deployments, you can use Ploomber Cloud. It allows you to deploy
marimo in a secure and scalable way. See
[deployment instructions here](https://docs.cloud.ploomber.io/en/latest/apps/marimo.html)
[deployment instructions here](https://docs.cloud.ploomber.io/en/latest/apps/marimo.html)
1 change: 0 additions & 1 deletion docs/guides/deploying/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,6 @@ WASM notebooks support most but not all Python features and packages. See our
For a turnkey cloud solution, try [Marimo Cloud](/guides/deploying/deploying_marimo_cloud.md).
```


## Programmatically running the marimo backend

When deploying marimo notebooks, you can run the marimo backend programmatically. This allows you to customize the backend to your needs and deploy it in your own environment.
Expand Down
1 change: 1 addition & 0 deletions frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@
"lz-string": "^1.5.0",
"mermaid": "^10.9.0",
"partysocket": "1.0.1",
"path-to-regexp": "^6.2.2",
"plotly.js": "^2.32.0",
"pyodide": "^0.25.1",
"react-arborist": "^3.4.0",
Expand Down
7 changes: 7 additions & 0 deletions frontend/pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

73 changes: 73 additions & 0 deletions frontend/src/plugins/layout/RoutesPlugin.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
/* Copyright 2024 Marimo. All rights reserved. */
import React, { PropsWithChildren, useEffect, useMemo, useState } from "react";

import { z } from "zod";
import { IStatelessPlugin, IStatelessPluginProps } from "../stateless-plugin";
import { TinyRouter } from "@/utils/routes";
import useEvent from "react-use-event-hook";

interface Data {
/**
* Route paths to render.
*/
routes: string[];
}

export class RoutesPlugin implements IStatelessPlugin<Data> {
tagName = "marimo-routes";

validator = z.object({
routes: z.array(z.string()),
});

render(props: IStatelessPluginProps<Data>): JSX.Element {
return <RoutesComponent {...props.data}>{props.children}</RoutesComponent>;
}
}

const RoutesComponent = ({
routes,
children,
}: PropsWithChildren<Data>): JSX.Element => {
const childCount = React.Children.count(children);
if (childCount !== routes.length) {
throw new Error(
`Expected ${routes.length} children, but got ${childCount}`,
);
}

const router = useMemo(() => new TinyRouter(routes), [routes]);
const [matched, setMatched] = useState<string | null>(() => {
const match = router.match(window.location);
return match ? match[1] : null;
});

const handleFindMatch = useEvent((location: Location) => {
const match = router.match(location);
setMatched(match ? match[1] : null);
});

useEffect(() => {
// Listen for route changes
const listener = (e: PopStateEvent | HashChangeEvent) => {
handleFindMatch(window.location);
};
window.addEventListener("hashchange", listener);
window.addEventListener("popstate", listener);
return () => {
window.removeEventListener("hashchange", listener);
window.removeEventListener("popstate", listener);
};
}, [handleFindMatch]);

if (!matched) {
// eslint-disable-next-line react/jsx-no-useless-fragment
return <></>;
}

const matchedIndex = routes.indexOf(matched);
const child = React.Children.toArray(children)[matchedIndex];

// eslint-disable-next-line react/jsx-no-useless-fragment
return <>{child}</>;
};
6 changes: 4 additions & 2 deletions frontend/src/plugins/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@ import { AnyWidgetPlugin } from "./impl/anywidget/AnyWidgetPlugin";
import { LazyPlugin } from "./layout/LazyPlugin";
import { NavigationMenuPlugin } from "@/plugins/layout/NavigationMenuPlugin";
import { initializeSidebarElement } from "./core/sidebar-element";
import { RoutesPlugin } from "./layout/RoutesPlugin";

// List of UI plugins
export const UI_PLUGINS: Array<IPlugin<any, unknown>> = [
Expand Down Expand Up @@ -81,11 +82,12 @@ const LAYOUT_PLUGINS: Array<IStatelessPlugin<unknown>> = [
new CarouselPlugin(),
new DownloadPlugin(),
new JsonOutputPlugin(),
new MermaidPlugin(),
new NavigationMenuPlugin(),
new ProgressPlugin(),
new RoutesPlugin(),
new StatPlugin(),
new TexPlugin(),
new MermaidPlugin(),
new NavigationMenuPlugin(),
];

export function initializePlugins() {
Expand Down
46 changes: 46 additions & 0 deletions frontend/src/utils/__tests__/routes.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
/* Copyright 2024 Marimo. All rights reserved. */
import { describe, it, expect } from "vitest";
import { TinyRouter } from "../routes";

describe("TinyRouter.match", () => {
it("should return the correct match and template for a given location", () => {
const templates = ["/path1", "/path2"];
const router = new TinyRouter(templates);

const location1 = { hash: "", pathname: "/path1" } as Location;
const location2 = { hash: "", pathname: "/path2" } as Location;

expect(router.match(location1)).toEqual([expect.any(Object), "/path1"]);
expect(router.match(location2)).toEqual([expect.any(Object), "/path2"]);
});

it("should return false if no match is found", () => {
const templates = ["/path1", "/path2"];
const router = new TinyRouter(templates);

const location = { hash: "", pathname: "/path3" } as Location;

expect(router.match(location)).toBe(false);
});

it("should return the correct match for hash locations", () => {
const templates = ["#/path1", "#/path2"];
const router = new TinyRouter(templates);

const location = { hash: "#/path1", pathname: "" } as Location;

expect(router.match(location)).toEqual([expect.any(Object), "#/path1"]);
});

it("order should matter for nested routes", () => {
const templates = ["/path1", "/path1/nested"];
const router = new TinyRouter(templates);

const location = { hash: "", pathname: "/path1/nested" } as Location;

expect(router.match(location)).toEqual([
expect.any(Object),
"/path1/nested",
]);
});
});
31 changes: 31 additions & 0 deletions frontend/src/utils/routes.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
/* Copyright 2024 Marimo. All rights reserved. */

import { match, Match, MatchFunction } from "path-to-regexp";

export class TinyRouter {
private routes: Array<{
template: string;
pathFunction: MatchFunction;
}>;

constructor(templates: string[]) {
this.routes = templates.map((template) => {
return {
template,
pathFunction: match(template),
};
});
}

match(location: Location): [Match, template: string] | false {
for (const { pathFunction, template } of this.routes) {
const match =
pathFunction(location.hash) || pathFunction(location.pathname);
if (match) {
return [match, template];
}
}

return false;
}
}
10 changes: 6 additions & 4 deletions frontend/vite.config.mts
Original file line number Diff line number Diff line change
Expand Up @@ -132,10 +132,12 @@ export default defineConfig({
},
},
},
headers: {
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp",
},
headers: isPyodide
? {
"Cross-Origin-Opener-Policy": "same-origin",
"Cross-Origin-Embedder-Policy": "require-corp",
}
: {},
},
define: {
"import.meta.env.VITE_MARIMO_VERSION": process.env.VITE_MARIMO_VERSION
Expand Down
2 changes: 2 additions & 0 deletions marimo/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,6 +49,7 @@
"refs",
"right",
"running_in_notebook",
"routes",
"sidebar",
"stat",
"state",
Expand Down Expand Up @@ -86,6 +87,7 @@
from marimo._plugins.stateless.nav_menu import nav_menu
from marimo._plugins.stateless.pdf import pdf
from marimo._plugins.stateless.plain_text import plain_text
from marimo._plugins.stateless.routes import routes
from marimo._plugins.stateless.sidebar import sidebar
from marimo._plugins.stateless.stat import stat
from marimo._plugins.stateless.style import style
Expand Down
Loading

0 comments on commit b71e1d9

Please sign in to comment.