Skip to content

Commit

Permalink
feat: admin page
Browse files Browse the repository at this point in the history
  • Loading branch information
atsuki44 committed Jan 22, 2025
1 parent e76ebfc commit 211a249
Show file tree
Hide file tree
Showing 15 changed files with 468 additions and 67 deletions.
17 changes: 11 additions & 6 deletions core/morph/api/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,7 @@ async def handle_other_error(_, exc):

@app.get("/", response_model=None)
async def index(inertia: InertiaDep) -> InertiaResponse:
return await inertia.render(
"index",
)
return await inertia.render("index", {"showAdminPage": is_local_dev_mode})


@app.get(
Expand All @@ -142,11 +140,18 @@ async def health_check():
app.include_router(router)


@app.get("/morph", response_model=None)
async def morph(inertia: InertiaDep) -> InertiaResponse:

if is_local_dev_mode:
return await inertia.render("morph", {"showAdminPage": True})

return await inertia.render("404", {"showAdminPage": False})


@app.get("/{full_path:path}", response_model=None)
async def subpages(full_path: str, inertia: InertiaDep) -> InertiaResponse:
return await inertia.render(
full_path,
)
return await inertia.render(full_path, {"showAdminPage": is_local_dev_mode})


handler = Mangum(app)
3 changes: 3 additions & 0 deletions core/morph/frontend/template/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,9 @@
"lint": "eslint . --ext ts,tsx --report-unused-disable-directives --max-warnings 0"
},
"dependencies": {
"@dagrejs/dagre": "^1.1.4",
"@xyflow/react": "^12.4.1",
"tailwind-variants": "^0.3.0",
"react": "^18.3.1",
"react-dom": "^18.3.1"
},
Expand Down
48 changes: 0 additions & 48 deletions core/morph/frontend/template/src/App.css

This file was deleted.

5 changes: 5 additions & 0 deletions core/morph/frontend/template/src/admin/AdminPage.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { DataPipeline } from "./datapipeline/DataPipeline";

export const AdminPage = () => {
return <DataPipeline />;
};
37 changes: 37 additions & 0 deletions core/morph/frontend/template/src/admin/common/useResourcesQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
import { useQuery } from "./utils/useQuery";

type Resource = {
alias: string;
path: string;
connection?: string | null;
output_paths: string[];
public?: boolean | null;
output_type?: string | null;
data_requirements?: string[] | null;
};

type GetResourcesResponse = {
resources: Resource[];
};

const getResources = async () => {
const response = await fetch("/cli/resource");

if (!response.ok) {
throw await response.json();
}

const data = await response.json();

if (data.error) {
throw data.error;
}

return data as GetResourcesResponse;
};

const useResourcesQuery = () => {
return useQuery(getResources);
};

export { type Resource, useResourcesQuery };
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { useQuery } from "./utils/useQuery";

type ScheduledJob = {
cron: string;
is_enabled?: boolean;
timezone?: string;
variables?: Record<string, unknown>;
};

type GetScheduledJobsResponse = Record<string, { schedules: ScheduledJob[] }>;

const getScheduledJobs = async () => {
const response = await fetch("/cli/morph-project/scheduled-jobs");

if (!response.ok) {
throw await response.json();
}

const data = await response.json();

if (data.error) {
throw data.error;
}

return data as GetScheduledJobsResponse;
};

const useScheduledJobsQuery = () => {
return useQuery(getScheduledJobs);
};

export { type ScheduledJob, useScheduledJobsQuery };
36 changes: 36 additions & 0 deletions core/morph/frontend/template/src/admin/common/utils/useQuery.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
import React from "react";

type QueryResult<T> =
| {
status: "loading";
}
| {
status: "success";
data: T;
}
| {
status: "error";
error: unknown;
};

export const useQuery = <T>(fetcher: () => T): QueryResult<Awaited<T>> => {
const [result, setResult] = React.useState<QueryResult<Awaited<T>>>({
status: "loading",
});

React.useEffect(() => {
const init = async () => {
try {
const data = await fetcher();

setResult({ status: "success", data });
} catch (error) {
setResult({ status: "error", error });
}
};

init();
}, [fetcher]);

return result;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import { ReactFlowProvider } from "@xyflow/react";
import { useResourcesQuery } from "../common/useResourcesQuery";

import "@xyflow/react/dist/style.css";
import { Flow } from "./Flow";
import { ResourceDetail } from "./ResourceDetail";

export const DataPipeline = () => {
const resources = useResourcesQuery();

if (resources.status === "loading") {
return null;
}

if (resources.status === "error") {
throw resources.error;
}

return (
<ReactFlowProvider>
{/* TODO: Refactor height calculation, avoid hardcoded values */}
<div className="grid grid-cols-3 gap-4 h-[calc(100vh-102px)]">
<div className="col-span-2">
<Flow resources={resources.data.resources} />
</div>
<div className="col-span-1 overflow-x-auto p-2">
<ResourceDetail resources={resources.data.resources} />
</div>
</div>
</ReactFlowProvider>
);
};
83 changes: 83 additions & 0 deletions core/morph/frontend/template/src/admin/datapipeline/Flow.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
import { ReactFlow, Background, Node, Edge } from "@xyflow/react";
import dagre from "@dagrejs/dagre";
import { Resource } from "../common/useResourcesQuery";

import "@xyflow/react/dist/style.css";
import { ResourceNode } from "./ResourceNode";

const nodeTypes = {
resource: ResourceNode,
};

export const Flow = ({ resources }: { resources: Resource[] }) => {
const nodes = convertResourcesToNodes(resources);
const edges = convertResourcesToEdges(resources);

return (
<ReactFlow
nodes={nodes}
edges={edges}
nodeTypes={nodeTypes}
className="rounded-lg"
fitView
>
<Background bgColor="#f5f5f5" />
</ReactFlow>
);
};

const convertResourcesToNodes = (resources: Resource[]): Node[] => {
const WIDTH = 200;
const HEIGHT = 40;

const graph = new dagre.graphlib.Graph().setDefaultEdgeLabel(() => ({}));

graph.setGraph({ rankdir: "TB", ranksep: HEIGHT, nodesep: WIDTH / 2 });

resources.forEach((resource) =>
graph.setNode(resource.alias, {
width: WIDTH,
height: HEIGHT,
})
);

resources.forEach((resource) => {
resource.data_requirements?.forEach((parentAlias) => {
graph.setEdge(parentAlias, resource.alias);
});
});

dagre.layout(graph);

const nodes: ResourceNode[] = resources.map((resource) => {
return {
id: resource.alias,
type: "resource",
position: {
x: graph.node(resource.alias).x,
y: graph.node(resource.alias).y,
},
width: WIDTH,
height: HEIGHT,
connectable: false,
draggable: false,
handles: [],
data: { resource },
};
});

return nodes;
};

const convertResourcesToEdges = (resources: Resource[]): Edge[] => {
return resources.reduce((edges, resource) => {
const addedEdges =
resource.data_requirements?.map((parentAlias) => ({
id: `${parentAlias}-${resource.alias}`,
source: parentAlias,
target: resource.alias,
})) ?? [];

return [...edges, ...addedEdges];
}, [] as Edge[]);
};
Loading

0 comments on commit 211a249

Please sign in to comment.