diff --git a/core/morph/api/app.py b/core/morph/api/app.py index 9ead037..9f4fe99 100644 --- a/core/morph/api/app.py +++ b/core/morph/api/app.py @@ -21,9 +21,6 @@ inertia_request_validation_exception_handler, inertia_version_conflict_exception_handler, ) -from starlette.middleware.cors import CORSMiddleware -from starlette.middleware.sessions import SessionMiddleware - from morph.api.error import ApiBaseError, InternalError, render_error_html from morph.api.handler import router from morph.api.plugin import plugin_app @@ -32,6 +29,8 @@ MorphFunctionMetaObjectCacheManager, MorphGlobalContext, ) +from starlette.middleware.cors import CORSMiddleware +from starlette.middleware.sessions import SessionMiddleware # configuration values @@ -42,10 +41,11 @@ # set true to MORPH_LOCAL_DEV_MODE to use local frontend server is_local_dev_mode = True if os.getenv("MORPH_LOCAL_DEV_MODE") == "true" else False +project_root = find_project_root_dir() + def custom_compile_logic(): logger.info("Compiling python and sql files...") - project_root = find_project_root_dir() context = MorphGlobalContext.get_instance() errors = context.load(project_root) if len(errors) > 0: @@ -129,8 +129,6 @@ async def lifespan(app: FastAPI) -> AsyncGenerator[None, None]: inertia_request_validation_exception_handler, ) -frontend_dir = os.path.join(os.getcwd(), ".morph", "frontend") - def get_inertia_config(): templates_dir = os.path.join(Path(__file__).resolve().parent, "templates") @@ -148,15 +146,16 @@ def get_inertia_config(): use_flash_messages=True, use_flash_errors=True, entrypoint_filename="main.tsx", - assets_prefix="/src", + root_directory=".morph/frontend", dev_url=frontend_url, ) return InertiaConfig( templates=Jinja2Templates(directory=templates_dir), - manifest_json_path=os.path.join(frontend_dir, "dist", "manifest.json"), + manifest_json_path=os.path.join(project_root, "dist", "manifest.json"), environment="production", entrypoint_filename="main.tsx", + root_directory=".morph/frontend", ) @@ -167,13 +166,13 @@ def get_inertia_config(): if is_local_dev_mode: app.mount( "/src", - StaticFiles(directory=os.path.join(frontend_dir, "src")), + StaticFiles(directory=os.path.join(project_root, "src")), name="src", ) else: app.mount( "/assets", - StaticFiles(directory=os.path.join(frontend_dir, "dist", "assets")), + StaticFiles(directory=os.path.join(project_root, "dist", "assets")), name="assets", ) diff --git a/core/morph/api/cloud/client.py b/core/morph/api/cloud/client.py index 8d60a34..d0d197c 100644 --- a/core/morph/api/cloud/client.py +++ b/core/morph/api/cloud/client.py @@ -132,14 +132,20 @@ def verify_api_secret(self) -> MorphClientResponse: @validate_project_id def initiate_deployment( - self, project_id: str, image_build_log: str, image_checksum: str + self, + project_id: str, + image_build_log: str, + image_checksum: str, + config: Optional[dict[str, Any]] = None, ) -> MorphClientResponse: path = "deployment" - body = { + body: dict[str, Any] = { "projectId": project_id, "imageBuildLog": image_build_log, "imageChecksum": image_checksum, } + if config: + body["config"] = config return self.request(method="POST", path=path, data=body) diff --git a/core/morph/api/service.py b/core/morph/api/service.py index 744aeea..d5a350e 100644 --- a/core/morph/api/service.py +++ b/core/morph/api/service.py @@ -7,7 +7,6 @@ import time import uuid from contextlib import redirect_stdout -from pathlib import Path from typing import Any import click @@ -284,11 +283,11 @@ async def file_upload_service(input: UploadFileService) -> Any: ) # Read the saved file path from the cache (always created as following path) - cache_file = Path(find_project_root_dir()).joinpath( - ".morph/cache/file_upload.md" - ) - with open(cache_file, "r") as f: - saved_filepath = f.read() + saved_filepath = "" + cache_file = "/tmp/file_upload.cache" + if os.path.exists(cache_file): + with open(cache_file, "r") as f: + saved_filepath = f.read() # Remove the temporary directory if os.path.exists(temp_dir): diff --git a/core/morph/api/templates/index.html b/core/morph/api/templates/index.html index 9132d42..8265186 100644 --- a/core/morph/api/templates/index.html +++ b/core/morph/api/templates/index.html @@ -16,13 +16,6 @@ window.__vite_plugin_react_preamble_installed__ = true; {% endif %} - - - {% inertia_body %} diff --git a/core/morph/config/project.py b/core/morph/config/project.py index 9c42bc5..7e6b914 100644 --- a/core/morph/config/project.py +++ b/core/morph/config/project.py @@ -1,5 +1,5 @@ import os -from typing import List, Optional +from typing import Dict, List, Optional import yaml from pydantic import BaseModel, Field @@ -13,6 +13,20 @@ from morph.task.utils.morph import find_project_root_dir +class BuildConfig(BaseModel): + runtime: Optional[str] = None + framework: Optional[str] = "morph" + package_manager: Optional[str] = None + context: Optional[str] = None + build_args: Optional[Dict[str, str]] = None + + +class DeploymentConfig(BaseModel): + provider: Optional[str] = "aws" + aws: Optional[Dict[str, Optional[str]]] = None + gcp: Optional[Dict[str, Optional[str]]] = None + + class MorphProject(BaseModel): profile: Optional[str] = "default" source_paths: List[str] = Field(default_factory=lambda: ["src"]) @@ -21,6 +35,8 @@ class MorphProject(BaseModel): package_manager: str = Field( default="pip", description="Package manager to use, e.g., pip or poetry." ) + build: Optional[BuildConfig] = Field(default_factory=BuildConfig) + deployment: Optional[DeploymentConfig] = Field(default_factory=DeploymentConfig) class Config: arbitrary_types_allowed = True @@ -72,9 +88,115 @@ def save_project(project_root: str, project: MorphProject) -> None: old_config_path = os.path.join(project_root, "morph_project.yaml") if os.path.exists(old_config_path): with open(old_config_path, "w") as f: - yaml.safe_dump(project.model_dump(), f) + f.write(dump_project_yaml(project)) return config_path = os.path.join(project_root, "morph_project.yml") with open(config_path, "w") as f: - yaml.safe_dump(project.model_dump(), f) + f.write(dump_project_yaml(project)) + + +def dump_project_yaml(project: MorphProject) -> str: + source_paths = "\n- ".join([""] + project.source_paths) + + # Default values + build_runtime = "" + build_framework = "" + build_package_manager = "" + build_context = "." + build_args_str = "\n # - ARG_NAME=value\n # - ANOTHER_ARG=value" + deployment_provider = "aws" + deployment_aws_region = "us-east-1" + deployment_aws_memory = "1024" + deployment_aws_timeout = "300" + deployment_aws_concurrency = "1" + deployment_gcp_region = "us-central1" + deployment_gcp_memory = "1Gi" + deployment_gcp_cpu = "1" + deployment_gcp_concurrency = "80" + deployment_gcp_timeout = "300" + + # Set values if build exists + if project.build: + if project.build.runtime: + build_runtime = project.build.runtime or "" + if project.build.framework: + build_framework = project.build.framework or "" + if project.build.package_manager: + build_package_manager = project.build.package_manager or "" + if project.build.context: + build_context = f"{project.build.context}" or "." + if project.build.build_args: + build_args_items = [] + for key, value in project.build.build_args.items(): + build_args_items.append(f"{key}={value}") + build_args_str = ( + "\n # - ".join([""] + build_args_items) + if build_args_items + else "\n # - ARG_NAME=value\n # - ANOTHER_ARG=value" + ) + + # Set values if deployment exists + if project.deployment: + if project.deployment.provider: + deployment_provider = project.deployment.provider or "aws" + if project.deployment.aws: + deployment_aws_region = project.deployment.aws.get("region") or "us-east-1" + deployment_aws_memory = project.deployment.aws.get("memory") or "1024" + deployment_aws_timeout = project.deployment.aws.get("timeout") or "300" + deployment_aws_concurrency = ( + project.deployment.aws.get("concurrency") or "1" + ) + if project.deployment.gcp: + deployment_gcp_region = ( + project.deployment.gcp.get("region") or "us-central1" + ) + deployment_gcp_memory = project.deployment.gcp.get("memory") or "1Gi" + deployment_gcp_cpu = project.deployment.gcp.get("cpu") or "1" + deployment_gcp_concurrency = ( + project.deployment.gcp.get("concurrency") or "80" + ) + deployment_gcp_timeout = project.deployment.gcp.get("timeout") or "300" + else: + # Use default DeploymentConfig + deployment_provider = "aws" + + return f""" +version: '1' + +# Framework Settings +default_connection: {project.default_connection} +source_paths:{source_paths} + +# Cloud Settings +# profile: {project.profile} # Defined in the Profile Section in `~/.morph/credentials` +# project_id: {project.project_id or "null"} + +# Build Settings +build: + # These settings are required when there is no Dockerfile in the project root. + # They define the environment in which the project will be built + runtime: {build_runtime} # python3.9, python3.10, python3.11, python3.12 + framework: {build_framework} + package_manager: {build_package_manager} # pip, poetry, uv + # These settings are required when there is a Dockerfile in the project root. + # They define how the Docker image will be built + # context: {build_context} + # build_args:{build_args_str} + +# Deployment Settings +deployment: + provider: {deployment_provider} # aws or gcp (default is aws) + # These settings are used only when you want to customize the deployment settings + # aws: + # region: {deployment_aws_region} + # memory: {deployment_aws_memory} + # timeout: {deployment_aws_timeout} + # concurrency: {deployment_aws_concurrency} + # gcp: + # region: {deployment_gcp_region} + # memory: {deployment_gcp_memory} + # cpu: {deployment_gcp_cpu} + # concurrency: {deployment_gcp_concurrency} + # timeout: {deployment_gcp_timeout} +""" diff --git a/core/morph/frontend/template/.eslintrc.cjs b/core/morph/frontend/template/.eslintrc.cjs deleted file mode 100644 index d6c9537..0000000 --- a/core/morph/frontend/template/.eslintrc.cjs +++ /dev/null @@ -1,18 +0,0 @@ -module.exports = { - root: true, - env: { browser: true, es2020: true }, - extends: [ - 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', - 'plugin:react-hooks/recommended', - ], - ignorePatterns: ['dist', '.eslintrc.cjs'], - parser: '@typescript-eslint/parser', - plugins: ['react-refresh'], - rules: { - 'react-refresh/only-export-components': [ - 'warn', - { allowConstantExport: true }, - ], - }, -} diff --git a/core/morph/frontend/template/.gitignore b/core/morph/frontend/template/.gitignore deleted file mode 100644 index 1e47915..0000000 --- a/core/morph/frontend/template/.gitignore +++ /dev/null @@ -1,32 +0,0 @@ -# Logs -logs -*.log -npm-debug.log* -yarn-debug.log* -yarn-error.log* -pnpm-debug.log* -lerna-debug.log* - -node_modules -dist -dist-ssr -*.local - -# Editor directories and files -.vscode/* -!.vscode/extensions.json -.idea -.DS_Store -*.suo -*.ntvs* -*.njsproj -*.sln -*.sw? - -# development -local-test - -# project files -src/pages/* -!src/pages/.gitkeep -constants.js diff --git a/core/morph/frontend/template/index.html b/core/morph/frontend/template/index.html deleted file mode 100644 index 8467e8f..0000000 --- a/core/morph/frontend/template/index.html +++ /dev/null @@ -1,12 +0,0 @@ - - - - - - Morph Project - - -
- - - diff --git a/core/morph/frontend/template/package.json b/core/morph/frontend/template/package.json deleted file mode 100644 index cf5af0e..0000000 --- a/core/morph/frontend/template/package.json +++ /dev/null @@ -1,41 +0,0 @@ -{ - "name": "morph-frontend", - "private": true, - "version": "0.0.0", - "type": "module", - "scripts": { - "dev": "vite --port", - "build": "tsc -b && vite build", - "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" - }, - "devDependencies": { - "@inertiajs/react": "^1.2.0", - "@mdx-js/rollup": "^3.1.0", - "@stefanprobst/rehype-extract-toc": "^2.2.1", - "@types/mdx": "^2.0.13", - "@types/node": "^20.14.11", - "@types/react": "^18.3.3", - "@types/react-dom": "^18.3.0", - "@typescript-eslint/eslint-plugin": "^7.15.0", - "@typescript-eslint/parser": "^7.15.0", - "@morph-data/components": "0.2.0-beta.2", - "@vitejs/plugin-react-swc": "^3.5.0", - "class-variance-authority": "^0.7.1", - "eslint": "^8.57.0", - "eslint-plugin-react-hooks": "^4.6.2", - "eslint-plugin-react-refresh": "^0.4.7", - "react-error-boundary": "^4.1.2", - "rehype-slug": "^6.0.0", - "remark-gfm": "^4.0.0", - "typescript": "^5.2.2", - "vite": "^5.3.4", - "vite-plugin-restart": "^0.4.2" - } -} diff --git a/core/morph/frontend/template/src/admin/AdminPage.tsx b/core/morph/frontend/template/src/admin/AdminPage.tsx deleted file mode 100644 index f42580a..0000000 --- a/core/morph/frontend/template/src/admin/AdminPage.tsx +++ /dev/null @@ -1,5 +0,0 @@ -import { DataPipeline } from "./datapipeline/DataPipeline"; - -export const AdminPage = () => { - return ; -}; diff --git a/core/morph/frontend/template/src/admin/common/useResourcesQuery.ts b/core/morph/frontend/template/src/admin/common/useResourcesQuery.ts deleted file mode 100644 index f1581c8..0000000 --- a/core/morph/frontend/template/src/admin/common/useResourcesQuery.ts +++ /dev/null @@ -1,37 +0,0 @@ -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 }; diff --git a/core/morph/frontend/template/src/admin/common/useScheduledJobsQuery.ts b/core/morph/frontend/template/src/admin/common/useScheduledJobsQuery.ts deleted file mode 100644 index 52eff98..0000000 --- a/core/morph/frontend/template/src/admin/common/useScheduledJobsQuery.ts +++ /dev/null @@ -1,32 +0,0 @@ -import { useQuery } from "./utils/useQuery"; - -type ScheduledJob = { - cron: string; - is_enabled?: boolean; - timezone?: string; - variables?: Record; -}; - -type GetScheduledJobsResponse = Record; - -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 }; diff --git a/core/morph/frontend/template/src/admin/common/utils/useQuery.ts b/core/morph/frontend/template/src/admin/common/utils/useQuery.ts deleted file mode 100644 index c2695cb..0000000 --- a/core/morph/frontend/template/src/admin/common/utils/useQuery.ts +++ /dev/null @@ -1,36 +0,0 @@ -import React from "react"; - -type QueryResult = - | { - status: "loading"; - } - | { - status: "success"; - data: T; - } - | { - status: "error"; - error: unknown; - }; - -export const useQuery = (fetcher: () => T): QueryResult> => { - const [result, setResult] = React.useState>>({ - 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; -}; diff --git a/core/morph/frontend/template/src/admin/datapipeline/DataPipeline.tsx b/core/morph/frontend/template/src/admin/datapipeline/DataPipeline.tsx deleted file mode 100644 index abc84a3..0000000 --- a/core/morph/frontend/template/src/admin/datapipeline/DataPipeline.tsx +++ /dev/null @@ -1,32 +0,0 @@ -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 ( - - {/* TODO: Refactor height calculation, avoid hardcoded values */} -
-
- -
-
- -
-
-
- ); -}; diff --git a/core/morph/frontend/template/src/admin/datapipeline/Flow.tsx b/core/morph/frontend/template/src/admin/datapipeline/Flow.tsx deleted file mode 100644 index 6601869..0000000 --- a/core/morph/frontend/template/src/admin/datapipeline/Flow.tsx +++ /dev/null @@ -1,83 +0,0 @@ -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 ( - - - - ); -}; - -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[]); -}; diff --git a/core/morph/frontend/template/src/admin/datapipeline/ResourceDetail.tsx b/core/morph/frontend/template/src/admin/datapipeline/ResourceDetail.tsx deleted file mode 100644 index 6ec9077..0000000 --- a/core/morph/frontend/template/src/admin/datapipeline/ResourceDetail.tsx +++ /dev/null @@ -1,120 +0,0 @@ -import { Node, useOnSelectionChange } from "@xyflow/react"; -import React from "react"; -import { useScheduledJobsQuery } from "../common/useScheduledJobsQuery"; -import { Resource } from "../common/useResourcesQuery"; - -export const ResourceDetail = ({ resources }: { resources: Resource[] }) => { - const [selectedAlias, setSelectedAlias] = React.useState(null); - - const onChange = React.useCallback(({ nodes }: { nodes: Node[] }) => { - if (nodes.length === 0) { - setSelectedAlias(null); - } else { - setSelectedAlias(nodes[0].id); - } - }, []); - - useOnSelectionChange({ - onChange, - }); - - if (!selectedAlias) { - return ( -
-

Select cell to view details

-
- ); - } - - return ( -
-

{selectedAlias}

- - {/* */} -
- ); -}; - -const BasicInfo = ({ - alias, - resources, -}: { - alias: string; - resources: Resource[]; -}) => { - const resource = resources.find((r) => r.alias === alias); - - if (!resource) { - return

Something went wrong

; - } - - return ( -
-

- Defined at {resource.path} -

-
- ); -}; - -const ScheduledJobs = ({ alias }: { alias: string }) => { - const scheduledJobs = useScheduledJobsQuery(); - - if (scheduledJobs.status === "loading") { - return null; - } - - if (scheduledJobs.status === "error") { - return

Something went wrong

; - } - - const scheduledJobsForAlias = scheduledJobs.data[alias]?.schedules ?? []; - - return ( -
-

Scheduled Jobs

- {scheduledJobsForAlias.length === 0 && ( -

No scheduled jobs

- )} - {scheduledJobsForAlias.map((job, i) => ( -
-

{`${job.cron} (${ - job.timezone ?? "UTC" - })`}

- - {job.variables ? ( -
- - Variables - - - - - - - - - - {Object.entries(job.variables).map(([key, value]) => ( - - - - - ))} - -
KeyValue
{key}{String(value)}
-
- ) : ( - - No variables - - )} - - {i < scheduledJobsForAlias.length - 1 && ( -
- )} -
- ))} -
- ); -}; diff --git a/core/morph/frontend/template/src/admin/datapipeline/ResourceNode.tsx b/core/morph/frontend/template/src/admin/datapipeline/ResourceNode.tsx deleted file mode 100644 index 048d6d3..0000000 --- a/core/morph/frontend/template/src/admin/datapipeline/ResourceNode.tsx +++ /dev/null @@ -1,79 +0,0 @@ -import { tv } from "tailwind-variants"; -import { Resource } from "../common/useResourcesQuery"; -import { - Handle, - Node, - NodeProps, - Position, - useOnSelectionChange, -} from "@xyflow/react"; -import React from "react"; -import databaseIcon from "../../assets/icons/database.svg"; -import pythonIcon from "../../assets/icons/python.svg"; - -export type ResourceNode = Node< - { - resource: Resource; - }, - "resource" ->; - -const useSelected = (id: string) => { - const [selected, setSelected] = React.useState(false); - - useOnSelectionChange({ - onChange: React.useCallback(({ nodes }) => { - setSelected(nodes.some((node) => node.id === id)); - }, []), - }); - - return selected; -}; - -const ResourceNode = ({ id, data }: NodeProps) => { - const { resource } = data; - - const selected = useSelected(id); - - return ( -
- - {resource.alias} - - -
- ); -}; - -const Icon = ({ resource }: { resource: Resource }) => { - if (resource.path.endsWith(".py")) { - return ; - } - - if (resource.path.endsWith(".sql")) { - return ; - } - - return null; -}; - -const card = tv({ - base: "bg-gray-200 p-2 rounded-md flex align-center justify-center gap-2", - variants: { - selected: { - true: "outline outline-gray-400", - }, - }, -}); - -export { ResourceNode }; diff --git a/core/morph/frontend/template/src/assets/icons/database.svg b/core/morph/frontend/template/src/assets/icons/database.svg deleted file mode 100644 index 0458bd5..0000000 --- a/core/morph/frontend/template/src/assets/icons/database.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/core/morph/frontend/template/src/assets/icons/python.svg b/core/morph/frontend/template/src/assets/icons/python.svg deleted file mode 100644 index e0e096a..0000000 --- a/core/morph/frontend/template/src/assets/icons/python.svg +++ /dev/null @@ -1 +0,0 @@ - diff --git a/core/morph/frontend/template/src/error-page.tsx b/core/morph/frontend/template/src/error-page.tsx deleted file mode 100644 index 170d807..0000000 --- a/core/morph/frontend/template/src/error-page.tsx +++ /dev/null @@ -1,44 +0,0 @@ -import React from "react"; - -type ErrorPageProps = React.PropsWithChildren<{ - routes: Array<{ path: string; title: string }>; -}>; - -export const ErrorPage: React.FC = () => { - return ( -
-

Page Not Found

-

Oops! The page you're looking for doesn't exist or has been moved.

-

Please select an exsiting page from below.

-
- {/* {props.routes.map((route) => ( - - - - ))} */} -
-

Trouble shoot

-
- Newly added files are not displayed. -
-

You need to re-start mdx dev server.

-
    -
  1. 1. Close preview window on code editor
  2. -
  3. - 2. Stop process with putting{" "} - - Ctrl + C - {" "} - to the terminal -
  4. -
  5. - 3. Open preview window on code editor again -
  6. -
-
-
-
- ); -}; - -export default ErrorPage; diff --git a/core/morph/frontend/template/src/main.tsx b/core/morph/frontend/template/src/main.tsx deleted file mode 100644 index 1eb916c..0000000 --- a/core/morph/frontend/template/src/main.tsx +++ /dev/null @@ -1,123 +0,0 @@ -import "vite/modulepreload-polyfill"; -import { createRoot } from "react-dom/client"; -import { createInertiaApp, Head } from "@inertiajs/react"; -import React, { StrictMode, useMemo } from "react"; -import { PageSkeleton } from "./page-skeleton.tsx"; -import "@morph-data/components/css"; -import { MDXComponents } from "mdx/types"; -import type { Toc } from "@stefanprobst/rehype-extract-toc"; -import { AdminPage } from "./admin/AdminPage.tsx"; -import { ErrorPage } from "./error-page.tsx"; -import { useRefresh } from "@morph-data/components"; -import { mdxComponents } from "./mdx-componens.tsx"; - -type MDXProps = { - children?: React.ReactNode; - components?: MDXComponents; -}; - -export type MDXComponent = (props: MDXProps) => JSX.Element; - -type PageModule = { - default: MDXComponent; - title?: string; - tableOfContents?: Toc; -}; // types MDX default export -type Pages = Record; - -const pages: Pages = import.meta.glob( - "/../../src/pages/**/*.mdx", - { - eager: true, - } -); - -const normalizePath = (filePath: string) => { - // const relativePath = filePath.replace(/\.mdx$/, "").replace(/^\.\/pages/, ""); - const relativePath = filePath - .replace(/\.mdx$/, "") - .replace(/^\.\.\/\.\.\/src\/pages/, ""); - - return relativePath === "/index" ? "/" : relativePath; -}; - -const routes = Object.entries(pages).map(([filePath, module]) => { - // Extract the exported title from the MDX file - const title = (() => { - if (module.title) { - return String(module.title); - } - - if (module.tableOfContents && module.tableOfContents.length > 0) { - const firstHeading = module.tableOfContents[0]; - if (firstHeading) { - return firstHeading.value; - } - } - - return "Untitled"; - })(); - - return { - path: normalizePath(filePath), - title, - toc: module.tableOfContents, - }; -}); - -document.addEventListener("DOMContentLoaded", () => { - createInertiaApp<{ showAdminPage: boolean }>({ - resolve: (name) => { - const pageModule = pages[`../../src/pages/${name}.mdx`]; - - const WrappedComponent: React.FC<{ showAdminPage: boolean }> = ({ - showAdminPage, - }) => { - useRefresh(); - - const firstHeading = useMemo(() => { - if ( - pageModule?.tableOfContents && - pageModule.tableOfContents.length > 0 - ) { - return pageModule.tableOfContents[0]; - } - return null; - }, []); - - const title = pageModule?.title || firstHeading?.value || "Untitled"; - - return ( - <> - - - - - {name === "morph" ? ( - - ) : pageModule ? ( - - ) : ( - - )} - - - ); - }; - - return WrappedComponent; - }, - setup({ el, App, props }) { - createRoot(el).render( - - - - ); - }, - }).then(() => {}); -}); diff --git a/core/morph/frontend/template/src/mdx-componens.tsx b/core/morph/frontend/template/src/mdx-componens.tsx deleted file mode 100644 index ad89a77..0000000 --- a/core/morph/frontend/template/src/mdx-componens.tsx +++ /dev/null @@ -1,105 +0,0 @@ -import { - Accordion, - Callout, - Card, - DataTable, - Embed, - Grid, - LLM, - Chat, - Metrics, - MetricsGrid, - Panel, - Input, - Select, - SelectGroup, - SelectGroupLabel, - SelectItem, - SelectSeparator, - SelectItems, - DatePicker, - DateRangePicker, - Pre, -} from "@morph-data/components"; -import { MDXComponents } from "mdx/types"; - -const builtinComponents = { - DataTable, - Embed, - Metrics, - MetricsGrid, - Input, - Select, - SelectGroup, - SelectGroupLabel, - SelectItem, - SelectSeparator, - SelectItems, - Card, - Grid, - Panel, - Accordion, - Callout, - LLM, - Chat, - DatePicker, - DateRangePicker, - pre: Pre, -} as const; - -type PluginModule = { - components?: Record; -}; - -type Plugins = { - [pluginName: string]: PluginModule; -}; - -const plugins: Plugins = Object.entries( - import.meta.glob( - "/../../src/plugin/**/react/index.ts", - { - eager: true, - } - ) -).reduce((acc, [reactEntryPath, module]) => { - // /path/to/plugin-name/react/index.ts -> plugin-name - const pluginName = reactEntryPath.match(/plugin\/(.+?)\//)?.[1] ?? ""; - return { - ...acc, - [pluginName]: module, - }; -}, {} as Plugins); - -const pluginsComponents = Object.entries(plugins).reduce( - (mdxComponents, [pluginName, module]) => { - if (!module.components) { - return mdxComponents; - } - - return Object.entries(module.components).reduce( - (mdxComponents, [componentName, component]) => { - const isComponentNameConflict = - Object.keys(mdxComponents).includes(componentName); - - if (isComponentNameConflict) { - console.warn( - `Component name conflict: ${componentName} in plugin ${pluginName}` - ); - } - - return { - ...mdxComponents, - [componentName]: component, - }; - }, - mdxComponents - ); - }, - {} as Record -); - -export const mdxComponents: MDXComponents = { - ...builtinComponents, - ...pluginsComponents, -}; diff --git a/core/morph/frontend/template/src/mdx.d.ts b/core/morph/frontend/template/src/mdx.d.ts deleted file mode 100644 index 838b4be..0000000 --- a/core/morph/frontend/template/src/mdx.d.ts +++ /dev/null @@ -1,4 +0,0 @@ -declare module "*.mdx" { - let MDXComponent: (props) => JSX.Element; - export default MDXComponent; -} diff --git a/core/morph/frontend/template/src/page-skeleton.tsx b/core/morph/frontend/template/src/page-skeleton.tsx deleted file mode 100644 index f7668e3..0000000 --- a/core/morph/frontend/template/src/page-skeleton.tsx +++ /dev/null @@ -1,102 +0,0 @@ -import React from "react"; -import { - DropdownMenu, - DropdownMenuTrigger, - DropdownMenuContent, - DropdownMenuItem, - Button, - DropdownMenuSeparator, - Toc, -} from "@morph-data/components"; -import { ErrorBoundary, FallbackProps } from "react-error-boundary"; -import { Link } from "@inertiajs/react"; -import { Toc as TocEntries } from "@stefanprobst/rehype-extract-toc"; - -function fallbackRender({ error }: FallbackProps) { - // Call resetErrorBoundary() to reset the error boundary and retry the render. - return ( -
-

Something went wrong:

-
{error.message}
-
- ); -} - -type PageSkeletonProps = React.PropsWithChildren<{ - routes: Array<{ path: string; title: string }>; - title: string; - showAdminPage: boolean; - toc?: TocEntries; -}>; - -export const PageSkeleton: React.FC = (props) => { - return ( - -
-
- - - - - - {props.routes.map((route) => ( - - - - {route.title} - - - - ))} - {props.showAdminPage && ( - <> - - - Admin Page - - - )} - - -
{props.title}
-
-
- Made with - - Morph - -
-
-
-
-
{props.children}
-
- -
-
-
-
-
- ); -}; diff --git a/core/morph/frontend/template/src/vite-env.d.ts b/core/morph/frontend/template/src/vite-env.d.ts deleted file mode 100644 index 11f02fe..0000000 --- a/core/morph/frontend/template/src/vite-env.d.ts +++ /dev/null @@ -1 +0,0 @@ -/// diff --git a/core/morph/frontend/template/tsconfig.app.json b/core/morph/frontend/template/tsconfig.app.json deleted file mode 100644 index 0c89611..0000000 --- a/core/morph/frontend/template/tsconfig.app.json +++ /dev/null @@ -1,24 +0,0 @@ -{ - "compilerOptions": { - "composite": true, - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.app.tsbuildinfo", - "target": "ES2020", - "useDefineForClassFields": true, - "lib": ["ES2020", "DOM", "DOM.Iterable"], - "module": "ESNext", - "skipLibCheck": true, - - /* Bundler mode */ - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, - "resolveJsonModule": true, - "isolatedModules": true, - "moduleDetection": "force", - "noEmit": true, - "jsx": "react-jsx", - - /* Linting */ - "strict": true - }, - "include": ["src"] -} diff --git a/core/morph/frontend/template/tsconfig.json b/core/morph/frontend/template/tsconfig.json deleted file mode 100644 index 2cb35ca..0000000 --- a/core/morph/frontend/template/tsconfig.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "files": [], - "references": [ - { - "path": "./tsconfig.app.json" - }, - { - "path": "./tsconfig.node.json" - } - ], -} diff --git a/core/morph/frontend/template/tsconfig.node.json b/core/morph/frontend/template/tsconfig.node.json deleted file mode 100644 index 3afdd6e..0000000 --- a/core/morph/frontend/template/tsconfig.node.json +++ /dev/null @@ -1,13 +0,0 @@ -{ - "compilerOptions": { - "composite": true, - "tsBuildInfoFile": "./node_modules/.tmp/tsconfig.node.tsbuildinfo", - "skipLibCheck": true, - "module": "ESNext", - "moduleResolution": "bundler", - "allowSyntheticDefaultImports": true, - "strict": true, - "noEmit": true - }, - "include": ["vite.config.ts"] -} diff --git a/core/morph/frontend/template/vite.config.ts b/core/morph/frontend/template/vite.config.ts deleted file mode 100644 index 52f6140..0000000 --- a/core/morph/frontend/template/vite.config.ts +++ /dev/null @@ -1,59 +0,0 @@ -import { defineConfig } from "vite"; -import react from "@vitejs/plugin-react-swc"; -import { resolve } from "path"; -import ViteRestart from "vite-plugin-restart"; -import mdx from "@mdx-js/rollup"; -import remarkGfm from "remark-gfm"; -import rehypeExtractToc from "@stefanprobst/rehype-extract-toc"; -import rehypeExtractTocMdxExport from "@stefanprobst/rehype-extract-toc/mdx"; -import rehypeSlug from "rehype-slug"; - -// https://vitejs.dev/config/ -export default defineConfig((env) => ({ - plugins: [ - react(), - { - enforce: "pre", - ...mdx({ - remarkPlugins: [remarkGfm], - rehypePlugins: [ - rehypeSlug, - rehypeExtractToc, - rehypeExtractTocMdxExport, - ], - }), - }, - ViteRestart({ - restart: ["../../src/pages/**/*"], - }), - ], - base: env.mode === "development" ? "" : "/_vite-static", - server: { - host: "0.0.0.0", - open: false, - watch: { - usePolling: true, - disableGlobbing: false, - }, - cors: true, - }, - resolve: { - alias: { - "@": resolve(__dirname, "./src"), - }, - }, - build: { - outDir: resolve("./dist"), - assetsDir: "assets", - target: "es2015", - manifest: "manifest.json", - rollupOptions: { - input: { - main: resolve("./src/main.tsx"), - }, - output: { - entryFileNames: `assets/bundle.js`, - }, - }, - }, -})); diff --git a/core/morph/include/Dockerfile b/core/morph/include/Dockerfile deleted file mode 100644 index 328e1cc..0000000 --- a/core/morph/include/Dockerfile +++ /dev/null @@ -1,15 +0,0 @@ -# Base image for Morph Cloud -FROM public.ecr.aws/i1l4z0u0/morph-data:python${MORPH_PYTHON_VERSION} - -# Set working directory -WORKDIR /var/task - -# Install Python dependencies -COPY requirements.txt . -RUN pip install --no-cache-dir -r requirements.txt --target "${MORPH_PACKAGE_ROOT}" - -# Copy source code and dependencies -COPY . . - -# Command to run the Lambda function -CMD python "${MORPH_APP_FILE_PATH}" diff --git a/core/morph/include/starter_template/.dockerignore b/core/morph/include/starter_template/.dockerignore deleted file mode 100644 index c6c3627..0000000 --- a/core/morph/include/starter_template/.dockerignore +++ /dev/null @@ -1,24 +0,0 @@ -# .morph -.morph/* -!.morph/frontend/dist -!.morph/core -!.morph/meta.json - -# node_modules -node_modules -package-lock.json -package.json - -.mock_user_context.json -README.md -.gitignore -.env - -# Python cache files -__pycache__/ -*.py[cod] -*$py.class -.pytest_cache/ -.coverage -.mypy_cache/ -.ruff_cache/ diff --git a/core/morph/include/starter_template/.gitignore b/core/morph/include/starter_template/.gitignore index 7acf8d2..b4386bf 100644 --- a/core/morph/include/starter_template/.gitignore +++ b/core/morph/include/starter_template/.gitignore @@ -1,6 +1,9 @@ # Morph config files and directories .morph +# Morph build output +dist + # Node.js node_modules diff --git a/core/morph/include/starter_template/components.json b/core/morph/include/starter_template/components.json new file mode 100644 index 0000000..67b484d --- /dev/null +++ b/core/morph/include/starter_template/components.json @@ -0,0 +1,21 @@ +{ + "$schema": "https://ui.shadcn.com/schema.json", + "style": "new-york", + "rsc": false, + "tsx": true, + "tailwind": { + "config": "", + "css": "src/pages/index.css", + "baseColor": "neutral", + "cssVariables": true, + "prefix": "" + }, + "aliases": { + "components": "@/pages/_components", + "utils": "@/pages/_lib/utils", + "ui": "@/pages/_components/ui", + "lib": "@/pages/_lib", + "hooks": "@/pages/_hooks" + }, + "iconLibrary": "lucide" +} diff --git a/core/morph/include/starter_template/package.json b/core/morph/include/starter_template/package.json index 52ebc56..fbd4e4b 100644 --- a/core/morph/include/starter_template/package.json +++ b/core/morph/include/starter_template/package.json @@ -4,11 +4,23 @@ "version": "0.0.0", "type": "module", "scripts": { - "build": "npm run build -w .morph/frontend" + "dev": "morph-frontend dev", + "build": "morph-frontend build" }, - "dependencies": {}, - "devDependencies": {}, - "workspaces": [ - ".morph/frontend" - ] + "devDependencies": { + "@tailwindcss/vite": "^4.0.14", + "@types/react": "^18.3.18", + "@types/react-dom": "^18.3.5", + "vite": "^6.2.1" + }, + "dependencies": { + "@morph-data/frontend": "0.3.0-beta.8", + "class-variance-authority": "^0.7.1", + "react": "^18.3.1", + "react-dom": "^18.3.1", + "react-error-boundary": "^5.0.0", + "tailwind-merge": "^3.0.2", + "tailwindcss": "^4.0.14", + "tailwindcss-animate": "^1.0.7" + } } diff --git a/core/morph/include/starter_template/src/pages/404.tsx b/core/morph/include/starter_template/src/pages/404.tsx new file mode 100644 index 0000000..4a669ab --- /dev/null +++ b/core/morph/include/starter_template/src/pages/404.tsx @@ -0,0 +1,8 @@ +export default function NotFound() { + return ( + <> +

404

+

Page not found

+ + ); +} diff --git a/core/morph/include/starter_template/src/pages/_app.tsx b/core/morph/include/starter_template/src/pages/_app.tsx new file mode 100644 index 0000000..57f0dc1 --- /dev/null +++ b/core/morph/include/starter_template/src/pages/_app.tsx @@ -0,0 +1,82 @@ +import { Head } from "@morph-data/frontend/components"; +import { TableOfContents } from "./_components/table-of-contents"; +import { Header } from "./_components/header"; +import { + usePageMeta, + MdxComponentsProvider, + Outlet, + useRefresh, + extractComponents, +} from "@morph-data/frontend/components"; +import { ErrorBoundary } from "react-error-boundary"; +import { Callout } from "@/pages/_components/ui/callout"; + +import "./index.css"; + +const uiComponents = extractComponents( + import.meta.glob("./_components/ui/**/*.tsx", { + eager: true, + }) +); + +const morphComponents = extractComponents( + import.meta.glob("./_components/*.tsx", { + eager: true, + }) +); + +export default function App() { + const pageMeta = usePageMeta(); + + useRefresh(); + + return ( + <> + + {pageMeta?.title} + + + +
+ + + {pageMeta && } + + + +
+
+
+ ( + + {typeof error.message === "string" + ? error.message + : "Something went wrong"} + + )} + > + + +
+
+ +
+
+
+
+
+ + ); +} + +export const Catch = () => ( + + Something went wrong + +); diff --git a/core/morph/include/starter_template/src/pages/_components/header.tsx b/core/morph/include/starter_template/src/pages/_components/header.tsx new file mode 100644 index 0000000..2be7531 --- /dev/null +++ b/core/morph/include/starter_template/src/pages/_components/header.tsx @@ -0,0 +1,86 @@ +import { + DropdownMenu, + DropdownMenuTrigger, + DropdownMenuContent, + DropdownMenuItem, +} from "@/pages/_components/ui/dropdown-menu"; +import { Button } from "@/pages/_components/ui/button"; +import { usePages, Link } from "@morph-data/frontend/components"; +import { PropsWithChildren } from "react"; + +const Root = ({ children }: PropsWithChildren) => { + return
{children}
; +}; + +const DropDownMenu = () => { + const pages = usePages(); + + return ( + + + + + + {pages.map((page) => ( + + + {page.title} + + + ))} + {/* {props.showAdminPage && ( + <> + + + Admin Page + + + )} */} + + + ); +}; + +const PageTitle = ({ title }: { title: string }) => { + return
{title}
; +}; + +const Spacer = () =>
; + +const MorphLogo = () => ( +
+ Made with + + Morph + +
+); + +export const Header = { + Root, + DropDownMenu, + PageTitle, + Spacer, + MorphLogo, +}; diff --git a/core/morph/include/starter_template/src/pages/_components/table-of-contents.tsx b/core/morph/include/starter_template/src/pages/_components/table-of-contents.tsx new file mode 100644 index 0000000..25ba07c --- /dev/null +++ b/core/morph/include/starter_template/src/pages/_components/table-of-contents.tsx @@ -0,0 +1,64 @@ +import { + HoverCard, + HoverCardTrigger, + HoverCardContent, +} from "@/pages/_components/ui/hover-card"; +import { Button } from "@/pages/_components/ui/button"; +import { LucideTableOfContents } from "lucide-react"; +import { cn } from "@/pages/_lib/utils"; +import { Toc } from "@morph-data/frontend/components"; + +export interface TocProps { + toc?: Toc; + className?: string; +} + +export const TableOfContents: React.FC = ({ toc, className }) => { + if (!toc) { + return null; + } + + return ( + <> + +
+ + + + + +
+ {toc.map((entry) => ( + + ))} +
+
+
+
+ + ); +}; + +const Heading = ({ entry }: { entry: Toc[number] }) => { + return ( + <> + + {entry.value} + + {entry.children?.map((child) => ( + + ))} + + ); +}; diff --git a/core/morph/include/starter_template/src/pages/_lib/utils.ts b/core/morph/include/starter_template/src/pages/_lib/utils.ts new file mode 100644 index 0000000..a5ef193 --- /dev/null +++ b/core/morph/include/starter_template/src/pages/_lib/utils.ts @@ -0,0 +1,6 @@ +import { clsx, type ClassValue } from "clsx"; +import { twMerge } from "tailwind-merge"; + +export function cn(...inputs: ClassValue[]) { + return twMerge(clsx(inputs)); +} diff --git a/core/morph/include/starter_template/src/pages/index.css b/core/morph/include/starter_template/src/pages/index.css new file mode 100644 index 0000000..f6a6be0 --- /dev/null +++ b/core/morph/include/starter_template/src/pages/index.css @@ -0,0 +1,214 @@ +@import "tailwindcss"; + +@plugin "tailwindcss-animate"; + +@custom-variant dark (&:is(.dark *)); + +:root { + --background: oklch(1 0 0); + --foreground: oklch(0.145 0 0); + --card: oklch(1 0 0); + --card-foreground: oklch(0.145 0 0); + --popover: oklch(1 0 0); + --popover-foreground: oklch(0.145 0 0); + --primary: oklch(0.205 0 0); + --primary-foreground: oklch(0.985 0 0); + --secondary: oklch(0.97 0 0); + --secondary-foreground: oklch(0.205 0 0); + --muted: oklch(0.97 0 0); + --muted-foreground: oklch(0.556 0 0); + --accent: oklch(0.97 0 0); + --accent-foreground: oklch(0.205 0 0); + --destructive: oklch(0.577 0.245 27.325); + --destructive-foreground: oklch(0.577 0.245 27.325); + --border: oklch(0.922 0 0); + --input: oklch(0.922 0 0); + --ring: oklch(0.708 0 0); + --chart-1: oklch(0.646 0.222 41.116); + --chart-2: oklch(0.6 0.118 184.704); + --chart-3: oklch(0.398 0.07 227.392); + --chart-4: oklch(0.828 0.189 84.429); + --chart-5: oklch(0.769 0.188 70.08); + --radius: 0.625rem; + --sidebar: oklch(0.985 0 0); + --sidebar-foreground: oklch(0.145 0 0); + --sidebar-primary: oklch(0.205 0 0); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.97 0 0); + --sidebar-accent-foreground: oklch(0.205 0 0); + --sidebar-border: oklch(0.922 0 0); + --sidebar-ring: oklch(0.708 0 0); +} + +.dark { + --background: oklch(0.145 0 0); + --foreground: oklch(0.985 0 0); + --card: oklch(0.145 0 0); + --card-foreground: oklch(0.985 0 0); + --popover: oklch(0.145 0 0); + --popover-foreground: oklch(0.985 0 0); + --primary: oklch(0.985 0 0); + --primary-foreground: oklch(0.205 0 0); + --secondary: oklch(0.269 0 0); + --secondary-foreground: oklch(0.985 0 0); + --muted: oklch(0.269 0 0); + --muted-foreground: oklch(0.708 0 0); + --accent: oklch(0.269 0 0); + --accent-foreground: oklch(0.985 0 0); + --destructive: oklch(0.396 0.141 25.723); + --destructive-foreground: oklch(0.637 0.237 25.331); + --border: oklch(0.269 0 0); + --input: oklch(0.269 0 0); + --ring: oklch(0.439 0 0); + --chart-1: oklch(0.488 0.243 264.376); + --chart-2: oklch(0.696 0.17 162.48); + --chart-3: oklch(0.769 0.188 70.08); + --chart-4: oklch(0.627 0.265 303.9); + --chart-5: oklch(0.645 0.246 16.439); + --sidebar: oklch(0.205 0 0); + --sidebar-foreground: oklch(0.985 0 0); + --sidebar-primary: oklch(0.488 0.243 264.376); + --sidebar-primary-foreground: oklch(0.985 0 0); + --sidebar-accent: oklch(0.269 0 0); + --sidebar-accent-foreground: oklch(0.985 0 0); + --sidebar-border: oklch(0.269 0 0); + --sidebar-ring: oklch(0.439 0 0); +} + +@theme inline { + --color-background: var(--background); + --color-foreground: var(--foreground); + --color-card: var(--card); + --color-card-foreground: var(--card-foreground); + --color-popover: var(--popover); + --color-popover-foreground: var(--popover-foreground); + --color-primary: var(--primary); + --color-primary-foreground: var(--primary-foreground); + --color-secondary: var(--secondary); + --color-secondary-foreground: var(--secondary-foreground); + --color-muted: var(--muted); + --color-muted-foreground: var(--muted-foreground); + --color-accent: var(--accent); + --color-accent-foreground: var(--accent-foreground); + --color-destructive: var(--destructive); + --color-destructive-foreground: var(--destructive-foreground); + --color-border: var(--border); + --color-input: var(--input); + --color-ring: var(--ring); + --color-chart-1: var(--chart-1); + --color-chart-2: var(--chart-2); + --color-chart-3: var(--chart-3); + --color-chart-4: var(--chart-4); + --color-chart-5: var(--chart-5); + --radius-sm: calc(var(--radius) - 4px); + --radius-md: calc(var(--radius) - 2px); + --radius-lg: var(--radius); + --radius-xl: calc(var(--radius) + 4px); + --color-sidebar: var(--sidebar); + --color-sidebar-foreground: var(--sidebar-foreground); + --color-sidebar-primary: var(--sidebar-primary); + --color-sidebar-primary-foreground: var(--sidebar-primary-foreground); + --color-sidebar-accent: var(--sidebar-accent); + --color-sidebar-accent-foreground: var(--sidebar-accent-foreground); + --color-sidebar-border: var(--sidebar-border); + --color-sidebar-ring: var(--sidebar-ring); + --animate-accordion-down: accordion-down 0.2s ease-out; + --animate-accordion-up: accordion-up 0.2s ease-out; + + @keyframes accordion-down { + from { + height: 0; + } + to { + height: var(--radix-accordion-content-height); + } + } + + @keyframes accordion-up { + from { + height: var(--radix-accordion-content-height); + } + to { + height: 0; + } + } +} + +@layer base { + * { + @apply border-border outline-ring/50; + + /* Pre-defined Styles for user generated contents */ + .morph-page { + h1 { + @apply mt-10 scroll-m-20 text-3xl font-bold tracking-tight lg:text-4xl; + } + h2 { + @apply mt-8 scroll-m-20 pb-2 text-2xl font-semibold tracking-tight transition-colors first:mt-0; + } + h3 { + @apply mt-6 scroll-m-20 text-xl font-semibold tracking-tight; + } + p { + @apply leading-relaxed [&:not(:first-child)]:mt-6; + } + a { + @apply font-medium underline underline-offset-4; + &.x-underline { + @apply no-underline; + } + } + blockquote { + @apply mt-6 border-l-2 pl-6 italic; + } + ul { + @apply my-3 ml-3 list-disc list-inside [&>li]:mt-2; + } + table { + @apply table-auto min-w-full text-sm text-left rtl:text-right py-4; + } + thead { + @apply [&_tr]:border-b; + } + th { + @apply py-2 px-2 text-left align-middle font-medium [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]; + } + tbody { + @apply [&_tr:last-child]:border-0; + } + td { + @apply p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]; + } + figure[data-rehype-pretty-code-figure] { + width: 100%; + max-width: 100%; + min-width: 100%; + } + pre { + @apply my-4; + width: 100%; + max-width: 100%; + min-width: 100%; + overflow-x: auto; + padding: 1rem 0; + border-radius: 0.5rem; + box-sizing: border-box; + + [data-line] { + padding: 0 1rem; + font-size: 0.8rem; + } + + > code { + display: block; + } + } + :not(pre) > code { + @apply font-mono text-sm rounded bg-gray-100 dark:bg-neutral-700 px-1.5 py-1; + } + } + } + body { + @apply bg-background text-foreground; + } +} diff --git a/core/morph/include/starter_template/tsconfig.json b/core/morph/include/starter_template/tsconfig.json new file mode 100644 index 0000000..98111af --- /dev/null +++ b/core/morph/include/starter_template/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "@morph-data/frontend/tsconfig.app.json", + "compilerOptions": { + "composite": true, + "baseUrl": ".", + "paths": { + "@/*": ["./src/*"] + } + }, + "include": ["src"] +} diff --git a/core/morph/include/starter_template/vite.config.ts b/core/morph/include/starter_template/vite.config.ts new file mode 100644 index 0000000..e68cbc2 --- /dev/null +++ b/core/morph/include/starter_template/vite.config.ts @@ -0,0 +1,13 @@ +import { morph } from "@morph-data/frontend/plugin"; +import { defineConfig } from "vite"; +import path from "path"; +import tailwindcss from "@tailwindcss/vite"; + +export default defineConfig({ + plugins: [morph(), tailwindcss()], + resolve: { + alias: { + "@": path.resolve(__dirname, "./src"), + }, + }, +}); diff --git a/core/morph/task/api.py b/core/morph/task/api.py index 13d7eae..9beba4b 100644 --- a/core/morph/task/api.py +++ b/core/morph/task/api.py @@ -9,10 +9,9 @@ import click from dotenv import dotenv_values, load_dotenv - from morph.cli.flags import Flags from morph.task.base import BaseTask -from morph.task.utils.morph import find_project_root_dir, initialize_frontend_dir +from morph.task.utils.morph import find_project_root_dir class ApiTask(BaseTask): @@ -43,15 +42,10 @@ def __init__(self, args: Flags): for e_key, e_val in env_vars.items(): os.environ[e_key] = str(e_val) - # Initialize the frontend directory - # Copy the frontend template to ~/.morph/frontend if it doesn't exist - self.frontend_dir = initialize_frontend_dir(self.project_root) - # for managing subprocesses self.processes: List[subprocess.Popen[str]] = [] def _find_available_port(self, start_port: int, max_port: int = 65535) -> int: - port = start_port while port <= max_port: @@ -119,7 +113,7 @@ def _run_frontend(self) -> None: try: subprocess.run( "npm install", - cwd=self.frontend_dir, + cwd=self.project_root, shell=True, check=True, ) @@ -130,9 +124,9 @@ def _run_frontend(self) -> None: exit(1) self._run_process( - ["npm", "run", "dev", "--port", f"{self.front_port}"], - cwd=self.frontend_dir, - is_debug=False, + ["npm", "run", "dev", "--", "--port", f"{self.front_port}"], + cwd=self.project_root, + is_debug=True, ) def _run_process( diff --git a/core/morph/task/deploy.py b/core/morph/task/deploy.py index f2d81f6..3996ff2 100644 --- a/core/morph/task/deploy.py +++ b/core/morph/task/deploy.py @@ -16,7 +16,8 @@ from morph.config.project import load_project from morph.task.base import BaseTask from morph.task.utils.file_upload import FileWithProgress -from morph.task.utils.morph import find_project_root_dir, initialize_frontend_dir +from morph.task.utils.load_dockerfile import get_dockerfile_from_api +from morph.task.utils.morph import find_project_root_dir class DeployTask(BaseTask): @@ -34,11 +35,11 @@ def __init__(self, args: Flags): sys.exit(1) # Load morph_project.yml or equivalent - project = load_project(self.project_root) - if not project: + self.project = load_project(self.project_root) + if not self.project: click.echo(click.style("Project configuration not found.", fg="red")) sys.exit(1) - elif project.project_id is None: + elif self.project.project_id is None: click.echo( click.style( "Error: No project id found. Please fill project_id in morph_project.yml.", @@ -46,13 +47,34 @@ def __init__(self, args: Flags): ) ) sys.exit(1) - self.package_manager = project.package_manager + self.package_manager = self.project.package_manager # Check Dockerfile existence - self.dockerfile = os.path.join(self.project_root, "Dockerfile") - if not os.path.exists(self.dockerfile): - click.echo(click.style(f"Error: {self.dockerfile} not found", fg="red")) - sys.exit(1) + self.dockerfile_path = os.path.join(self.project_root, "Dockerfile") + self.use_custom_dockerfile = os.path.exists(self.dockerfile_path) + if self.use_custom_dockerfile: + provider = "aws" + if ( + self.project.deployment is not None + and self.project.deployment.provider is not None + ): + provider = self.project.deployment.provider or "aws" + if self.project.build is None: + dockerfile, dockerignore = get_dockerfile_from_api( + "morph", provider, None, None + ) + else: + dockerfile, dockerignore = get_dockerfile_from_api( + self.project.build.framework or "morph", + provider, + self.project.build.package_manager, + self.project.build.runtime, + ) + with open(self.dockerfile_path, "w") as f: + f.write(dockerfile) + dockerignore_path = os.path.join(self.project_root, ".dockerignore") + with open(dockerignore_path, "w") as f: + f.write(dockerignore) # Check Docker availability try: @@ -83,9 +105,6 @@ def __init__(self, args: Flags): click.echo(click.style(f"Error: {str(e)}", fg="red")) sys.exit(1) - # Frontend and backend settings - self.frontend_dir = initialize_frontend_dir(self.project_root) - # Docker settings self.image_name = f"{os.path.basename(self.project_root)}:latest" self.output_tar = os.path.join( @@ -109,7 +128,7 @@ def run(self): click.echo(click.style("Initiating deployment sequence...", fg="blue")) # 1. Build the source code - self._copy_and_build_source() + self._build_source() # 2. Build the Docker image click.echo(click.style("Building Docker image...", fg="blue")) @@ -129,6 +148,7 @@ def run(self): project_id=self.client.project_id, image_build_log=image_build_log, image_checksum=image_checksum, + config=self.project.model_dump() if self.project else None, ) except Exception as e: click.echo( @@ -390,31 +410,8 @@ def _validate_api_key(self): ) sys.exit(1) - def _copy_and_build_source(self): - click.echo(click.style("Building frontend...", fg="blue")) - try: - # Run npm install and build - subprocess.run( - ["npm", "install"], - cwd=self.project_root, - check=True, - shell=True if sys.platform == "win32" else False, - ) - subprocess.run( - ["npm", "run", "build"], - cwd=self.project_root, - check=True, - shell=True if sys.platform == "win32" else False, - ) - - except subprocess.CalledProcessError as e: - click.echo(click.style(f"Error building frontend: {str(e)}", fg="red")) - sys.exit(1) - except Exception as e: - click.echo(click.style(f"Unexpected error: {str(e)}", fg="red")) - sys.exit(1) - - click.echo(click.style("Building backend...", fg="blue")) + def _build_source(self): + click.echo(click.style("Compiling morph project...", fg="blue")) try: # Compile the morph project subprocess.run( @@ -440,7 +437,7 @@ def _build_docker_image(self) -> str: "-t", self.image_name, "-f", - self.dockerfile, + self.dockerfile_path, self.project_root, ] if self.no_cache: diff --git a/core/morph/task/new.py b/core/morph/task/new.py index 68d95e9..711c466 100644 --- a/core/morph/task/new.py +++ b/core/morph/task/new.py @@ -8,12 +8,15 @@ from typing import Optional import click - from morph.cli.flags import Flags -from morph.config.project import default_initial_project, load_project, save_project +from morph.config.project import ( + BuildConfig, + default_initial_project, + load_project, + save_project, +) from morph.constants import MorphConstant from morph.task.base import BaseTask -from morph.task.utils.morph import initialize_frontend_dir from morph.task.utils.run_backend.state import MorphGlobalContext @@ -33,10 +36,6 @@ def __init__(self, args: Flags, project_directory: Optional[str]): os.makedirs(morph_dir) click.echo(f"Created directory at {morph_dir}") - # Initialize the frontend directory - # Copy the frontend template to ~/.morph/frontend if it doesn't exist - initialize_frontend_dir(self.project_root) - # Select the Python version for the project self.selected_python_version = self._select_python_version() @@ -111,42 +110,12 @@ def run(self): ) project.package_manager = "poetry" - save_project(self.project_root, project) - - # Generate the Dockerfile template - template_dir = Path(__file__).parents[1].joinpath("include") - docker_template_file = template_dir.joinpath("Dockerfile") - if not docker_template_file.exists(): - click.echo( - click.style( - f"Template file not found: {docker_template_file}", fg="red" - ) - ) - click.echo() - sys.exit(1) - - # Generate the Dockerfile with the selected Python version - dockerfile_path = os.path.join(self.project_root, "Dockerfile") - try: - with docker_template_file.open("r", encoding="utf-8") as f: - dockerfile_content = f.read() - - # Replace the placeholder with the selected Python version - dockerfile_content = dockerfile_content.replace( - "${MORPH_PYTHON_VERSION}", self.selected_python_version - ) + if project.build is None: + project.build = BuildConfig() + project.build.package_manager = project.package_manager + project.build.runtime = f"python{self.selected_python_version}" - # Write the updated Dockerfile to the project directory - with open(dockerfile_path, "w") as output_file: - output_file.write(dockerfile_content) - except FileNotFoundError as e: - click.echo( - click.style(f"Error: Template Dockerfile not found: {e}", fg="red") - ) - sys.exit(1) - except IOError as e: - click.echo(click.style(f"Error: Unable to write Dockerfile: {e}", fg="red")) - sys.exit(1) + save_project(self.project_root, project) try: morph_data_version = importlib.metadata.version("morph-data") @@ -263,6 +232,25 @@ def run(self): ) sys.exit(1) + # Setup Frontend + subprocess.run( + [ + "npm", + "install", + ], + cwd=self.project_root, + ) + subprocess.run( + [ + "npx", + "shadcn@latest", + "add", + "--yes", + "https://morph-components.vercel.app/r/morph-components.json", + ], + cwd=self.project_root, + ) + click.echo() click.echo(click.style("Project setup completed successfully! 🎉", fg="green")) return True diff --git a/core/morph/task/utils/load_dockerfile.py b/core/morph/task/utils/load_dockerfile.py new file mode 100644 index 0000000..cfbbddb --- /dev/null +++ b/core/morph/task/utils/load_dockerfile.py @@ -0,0 +1,43 @@ +from typing import Any, Dict, Optional, Tuple + +import requests + + +def get_dockerfile_from_api( + framework: str, + provider: str, + package_manager: Optional[str] = None, + runtime: Optional[str] = None, +) -> Tuple[str, str]: + """ + Fetch dockerfile and dockerignore from the Morph API. + + Args: + framework: The framework to get the dockerfile for + provider: The provider to get the dockerfile for + package_manager: Optional package manager to use + runtime: Optional runtime to use + + Returns: + Tuple containing (dockerfile, dockerignore) + """ + url = f"https://dockerfile-template.morph-cb9.workers.dev/dockerfile/{framework}" + + params: Dict[str, Any] = { + "provider": provider, + } + if package_manager: + params["packageManager"] = package_manager + if runtime: + params["runtime"] = runtime + + response = requests.get(url, params=params) + + response.raise_for_status() + + data = response.json() + + if "error" in data: + raise ValueError(data["error"]) + + return data["dockerfile"], data["dockerignore"] diff --git a/core/morph/task/utils/morph.py b/core/morph/task/utils/morph.py index a8cf637..c5f573c 100644 --- a/core/morph/task/utils/morph.py +++ b/core/morph/task/utils/morph.py @@ -2,13 +2,11 @@ import logging import os import re -import shutil from pathlib import Path from typing import List, Optional, Union -from pydantic import BaseModel - from morph.constants import MorphConstant +from pydantic import BaseModel IGNORE_DIRS = ["/private/tmp", "/tmp"] @@ -41,23 +39,6 @@ def find_project_root_dir(abs_filepath: Optional[str] = None) -> str: ) -def initialize_frontend_dir(project_root: str) -> str: - """ - Initialize the frontend directory by copying the template frontend directory to the project directory. - Does nothing if the frontend directory already exists. - @param project_root: - @return: - """ - frontend_template_dir = os.path.join( - Path(__file__).resolve().parents[2], "frontend", "template" - ) - frontend_dir = MorphConstant.frontend_dir(project_root) - if not os.path.exists(frontend_dir): - os.makedirs(frontend_dir, exist_ok=True) - shutil.copytree(frontend_template_dir, frontend_dir, dirs_exist_ok=True) - return frontend_dir - - class Resource(BaseModel): alias: str path: str diff --git a/pyproject.toml b/pyproject.toml index ccca382..a9a39d3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,6 +1,6 @@ [tool.poetry] name = "morph-data" -version = "0.2.1" +version = "0.3.0" description = "Morph is a python-centric full-stack framework for building and deploying data apps." authors = ["Morph "] packages = [