Docker build
All checks were successful
/ Push Docker image to local registry (push) Successful in 3m28s

This commit is contained in:
kalle 2025-04-13 15:18:39 +02:00
parent ea438cb8b9
commit 6a24fec70e
9 changed files with 103 additions and 17 deletions

9
.dockerignore Normal file
View file

@ -0,0 +1,9 @@
.git
frontend/node_modules
frontend/dist
backend/target
backend/run
backend/data
backend/.env

View file

@ -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"

24
Dockerfile Normal file
View file

@ -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"]

View file

@ -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<dyn Drop + Send + Sync>,
}
@ -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;
}

View file

@ -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",

View file

@ -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 (
<div>
@ -152,6 +153,7 @@ function PropertyItem(props: PropertyItemProps) {
function DangerSection(props: { collectionId: CollectionId }) {
const collections = useCollections();
const navigate = useNavigate();
return (
<div>
@ -182,8 +184,10 @@ function DangerSection(props: { collectionId: CollectionId }) {
<AlertDialogAction asChild>
<Button
variant="destructive"
// TODO: Navigate away from the page when clicked
onClick={() => deleteCollection(collections, props.collectionId)}
onClick={() => {
navigate({ to: "/app" });
deleteCollection(collections, props.collectionId);
}}
>
Delete
</Button>

View file

@ -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<CollectionId | undefined>();
const [primaryColor, setPrimaryColor] = useState<ColorName | undefined>();
const [secondaryColor, setSecondaryColor] = useState<ColorName | undefined>();
const [type, setType] = useState<"text" | "canvas">("text");
const [type, setType] = useState<NoteType>("text");
const navigate = useNavigate();
function submit() {
@ -102,6 +102,10 @@ export function NewNoteDialog(props: NewNoteDialogProps) {
<RadioGroupItem value="canvas" id="type-canvas" />
<Label htmlFor="type-canvas">Canvas</Label>
</div>
<div className="flex items-center space-x-2">
<RadioGroupItem value="entity" id="type-entity" />
<Label htmlFor="type-entity">Entity</Label>
</div>
</RadioGroup>
</div>
</div>

View file

@ -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<NoteMetadata>
export type NoteMetadata = YMap<{
@ -20,7 +22,7 @@ export type NoteMetadata = YMap<{
secondaryColor: ColorName | ""
pinned: boolean
properties: YArray<NoteProperty>
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());

View file

@ -31,6 +31,9 @@ function Content(props: { id: string }) {
case "canvas": return <>
<NoteCanvas noteId={props.id} />
</>;
case "entity": return <>
Entity type
</>;
default: return null;
}
}