From 6a24fec70e0a77535791ae6ad562585dd4f1971a Mon Sep 17 00:00:00 2001 From: Kalle Struik Date: Sun, 13 Apr 2025 15:18:39 +0200 Subject: [PATCH] Docker build --- .dockerignore | 9 +++++ .forgejo/workflows/publish-docker.yml | 36 +++++++++++++++++++ Dockerfile | 24 +++++++++++++ backend/src/main.rs | 7 ++-- frontend/package.json | 1 + .../collection/collection_settings_dialog.tsx | 20 ++++++----- .../src/components/note/new_note_dialog.tsx | 8 +++-- frontend/src/lib/metadata.ts | 12 ++++--- frontend/src/routes/app/note.$id.tsx | 3 ++ 9 files changed, 103 insertions(+), 17 deletions(-) create mode 100644 .dockerignore create mode 100644 .forgejo/workflows/publish-docker.yml create mode 100644 Dockerfile diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..83e2f0a --- /dev/null +++ b/.dockerignore @@ -0,0 +1,9 @@ +.git + +frontend/node_modules +frontend/dist + +backend/target +backend/run +backend/data +backend/.env diff --git a/.forgejo/workflows/publish-docker.yml b/.forgejo/workflows/publish-docker.yml new file mode 100644 index 0000000..9c65929 --- /dev/null +++ b/.forgejo/workflows/publish-docker.yml @@ -0,0 +1,36 @@ +nam: Publish Docker image + +on: + push: + branches: + - 'main' + +jobs: + push_to_registry: + name: Push Docker image to local registry + runs-on: ubuntu-latest + steps: + - name: Check out the repo + uses: actions/checkout@v3 + with: + submodules: recursive + + - name: setup buildx + uses: https://github.com/docker/setup-buildx-action@v2 + + - name: Log in to local registry + run: | + BASE64_AUTH=`echo -n "$CI_REGISTRY_USER:$CI_REGISTRY_PASSWORD" | base64` + mkdir -p ~/.docker + echo "{\"auths\": {\"$CI_REGISTRY\": {\"auth\": \"$BASE64_AUTH\"}}}" > ~/.docker/config.json + env: + CI_REGISTRY: https://git.kallestruik.nl + CI_REGISTRY_USER: ${{ secrets.FORGEJO_USERNAME }} + CI_REGISTRY_PASSWORD: ${{ secrets.FORGEJO_PASSWORD }} + + - name: Build and push Docker image + uses: https://github.com/docker/build-push-action@v4 + with: + context: . + push: true + tags: "git.kallestruik.nl/knotes/v3:latest" diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..c5b7a05 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,24 @@ +FROM docker.io/library/node:23-alpine3.21 AS frontend_builder +WORKDIR /build/frontend +COPY frontend . +RUN corepack enable +RUN pnpm install +RUN pnpm run build + +FROM docker.io/library/rust:1.86-slim-bookworm as backend_builder +WORKDIR /build/backend +COPY backend . +RUN apt update && apt install -y clang libclang-dev +RUN cargo build --release + +FROM docker.io/library/debian:bookworm-slim as runner +WORKDIR /app +COPY --from=frontend_builder /build/frontend/dist ./frontend/ +COPY --from=backend_builder /build/backend/target/release/knotes-backend ./knotes-backend + +ENV DATA_DIR=/data/docs +ENV FRONTEND_DIR=/app/frontend + +EXPOSE 9000 + +CMD ["./knotes-backend"] diff --git a/backend/src/main.rs b/backend/src/main.rs index fd42356..cb05424 100644 --- a/backend/src/main.rs +++ b/backend/src/main.rs @@ -1,6 +1,6 @@ use base64::prelude::*; use chrono::serde::ts_seconds; -use chrono::{DateTime, Local, Utc}; +use chrono::{DateTime, Utc}; use cookie::time::{Duration, OffsetDateTime}; use dotenvy::dotenv; use futures_util::StreamExt; @@ -98,6 +98,7 @@ struct Connection { bcast: BroadcastGroup, // This is purely here to keep the connection to the DB alive while there is at least one // connection. + #[allow(dyn_drop)] _db_subscription: Arc, } @@ -192,7 +193,8 @@ async fn create_openid_client( #[tokio::main] async fn main() { - dotenv().expect("Failed to load .env file"); + // Allow loading .env to fail, since it won't be available in docker. + let _ = dotenv(); let data_dir = env::var("DATA_DIR").expect("DATA_DIR not set"); let data_dir = PathBuf::from_str(&data_dir).expect("DATA_DIR is not a valid path"); @@ -264,6 +266,7 @@ async fn main() { let routes = api_routes.or(ws).or(frontend_files).or(index); + println!("Starting server on http://0.0.0.0:9000"); warp::serve(routes).run(([0, 0, 0, 0], 9000)).await; } diff --git a/frontend/package.json b/frontend/package.json index cd87789..3ccfb61 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -2,6 +2,7 @@ "name": "knotes-frontend", "private": true, "type": "module", + "packageManager": "pnpm@10.6.3", "scripts": { "start": "vite --port 9000", "build": "vite build && tsc", diff --git a/frontend/src/components/collection/collection_settings_dialog.tsx b/frontend/src/components/collection/collection_settings_dialog.tsx index 7d02a9f..30eae3b 100644 --- a/frontend/src/components/collection/collection_settings_dialog.tsx +++ b/frontend/src/components/collection/collection_settings_dialog.tsx @@ -13,6 +13,7 @@ import { mdiPin, mdiPinOff, mdiPlus, mdiTrashCan } from "@mdi/js"; import { createCollectionProperty, deleteCollectionProperty } from "~/lib/property"; import { useEffect } from "react"; import { PropertyTypeCombobox } from "../form/property_type_combobox"; +import { useNavigate } from "@tanstack/react-router"; type CollectionSettingsDialogProps = { name?: string; @@ -78,18 +79,18 @@ function SettingsSection(props: { collectionId: CollectionId }) { function PropertiesSection(props: { collectionId: CollectionId }) { const collection = useCollection(props.collectionId) - if (!collection) { - return null; - } - // Ensure that the properties array exists on the collection useEffect(() => { - if (!collection.has("properties")) { + if (collection && !collection.has("properties")) { collection.set("properties", new Y.Array() as any) } }, [collection]) - const properties = collection.get("properties"); + const properties = collection?.get("properties"); + + if (!collection) { + return null; + } return (
@@ -152,6 +153,7 @@ function PropertyItem(props: PropertyItemProps) { function DangerSection(props: { collectionId: CollectionId }) { const collections = useCollections(); + const navigate = useNavigate(); return (
@@ -182,8 +184,10 @@ function DangerSection(props: { collectionId: CollectionId }) { diff --git a/frontend/src/components/note/new_note_dialog.tsx b/frontend/src/components/note/new_note_dialog.tsx index 21bd699..f3a7018 100644 --- a/frontend/src/components/note/new_note_dialog.tsx +++ b/frontend/src/components/note/new_note_dialog.tsx @@ -8,7 +8,7 @@ import type { ColorName } from "~/lib/color"; import { IconPicker } from "~/components/form/icon_picker"; import type { IconName } from "~/lib/icon"; import { CollectionPicker } from "~/components/form/collection_picker"; -import { type CollectionId, createNote } from "~/lib/metadata"; +import { type CollectionId, createNote, type NoteType } from "~/lib/metadata"; import { useNotesMetadata } from "~/hooks/use-metadata"; import { RadioGroup, RadioGroupItem } from "~/components/ui/radio-group"; import { useNavigate } from "@tanstack/react-router"; @@ -25,7 +25,7 @@ export function NewNoteDialog(props: NewNoteDialogProps) { const [collection, setCollection] = useState(); const [primaryColor, setPrimaryColor] = useState(); const [secondaryColor, setSecondaryColor] = useState(); - const [type, setType] = useState<"text" | "canvas">("text"); + const [type, setType] = useState("text"); const navigate = useNavigate(); function submit() { @@ -102,6 +102,10 @@ export function NewNoteDialog(props: NewNoteDialogProps) {
+
+ + +
diff --git a/frontend/src/lib/metadata.ts b/frontend/src/lib/metadata.ts index f1b71fe..308ae3c 100644 --- a/frontend/src/lib/metadata.ts +++ b/frontend/src/lib/metadata.ts @@ -9,6 +9,8 @@ export type NoteId = string export type CollectionId = string export type PropertyId = string +export type NoteType = "text" | "canvas" | "entity" + export type NotesMetadata = YArray export type NoteMetadata = YMap<{ @@ -20,7 +22,7 @@ export type NoteMetadata = YMap<{ secondaryColor: ColorName | "" pinned: boolean properties: YArray - type: "text" | "canvas" + type: NoteType }> export type NoteProperty = YMap<{ @@ -61,13 +63,13 @@ export function createCollection(md: CollectionsMetadata, data: { return collection; } -export function deleteCollection(md: CollectionsMetadata, id: CollectionId) { - const index = md.toArray().findIndex(it => it.get("id") == id); +export function deleteCollection(cmd: CollectionsMetadata, id: CollectionId) { + const index = cmd.toArray().findIndex(it => it.get("id") == id); if (index == -1) { return; } - md.delete(index, 1) + cmd.delete(index, 1) } export function createNote(md: NotesMetadata, data: { @@ -76,7 +78,7 @@ export function createNote(md: NotesMetadata, data: { collectionId: CollectionId | undefined, primaryColor: ColorName | undefined, secondaryColor: ColorName | undefined, - type: "text" | "canvas" + type: NoteType }) { const note = new Y.Map() as any as NoteMetadata; note.set("id", randomUUID()); diff --git a/frontend/src/routes/app/note.$id.tsx b/frontend/src/routes/app/note.$id.tsx index b2989f6..e137d12 100644 --- a/frontend/src/routes/app/note.$id.tsx +++ b/frontend/src/routes/app/note.$id.tsx @@ -31,6 +31,9 @@ function Content(props: { id: string }) { case "canvas": return <> ; + case "entity": return <> + Entity type + ; default: return null; } }