Docker build
All checks were successful
/ Push Docker image to local registry (push) Successful in 3m28s
All checks were successful
/ Push Docker image to local registry (push) Successful in 3m28s
This commit is contained in:
parent
ea438cb8b9
commit
6a24fec70e
9 changed files with 103 additions and 17 deletions
9
.dockerignore
Normal file
9
.dockerignore
Normal file
|
@ -0,0 +1,9 @@
|
||||||
|
.git
|
||||||
|
|
||||||
|
frontend/node_modules
|
||||||
|
frontend/dist
|
||||||
|
|
||||||
|
backend/target
|
||||||
|
backend/run
|
||||||
|
backend/data
|
||||||
|
backend/.env
|
36
.forgejo/workflows/publish-docker.yml
Normal file
36
.forgejo/workflows/publish-docker.yml
Normal 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
24
Dockerfile
Normal 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"]
|
|
@ -1,6 +1,6 @@
|
||||||
use base64::prelude::*;
|
use base64::prelude::*;
|
||||||
use chrono::serde::ts_seconds;
|
use chrono::serde::ts_seconds;
|
||||||
use chrono::{DateTime, Local, Utc};
|
use chrono::{DateTime, Utc};
|
||||||
use cookie::time::{Duration, OffsetDateTime};
|
use cookie::time::{Duration, OffsetDateTime};
|
||||||
use dotenvy::dotenv;
|
use dotenvy::dotenv;
|
||||||
use futures_util::StreamExt;
|
use futures_util::StreamExt;
|
||||||
|
@ -98,6 +98,7 @@ struct Connection {
|
||||||
bcast: BroadcastGroup,
|
bcast: BroadcastGroup,
|
||||||
// This is purely here to keep the connection to the DB alive while there is at least one
|
// This is purely here to keep the connection to the DB alive while there is at least one
|
||||||
// connection.
|
// connection.
|
||||||
|
#[allow(dyn_drop)]
|
||||||
_db_subscription: Arc<dyn Drop + Send + Sync>,
|
_db_subscription: Arc<dyn Drop + Send + Sync>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -192,7 +193,8 @@ async fn create_openid_client(
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn 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 = 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");
|
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);
|
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;
|
warp::serve(routes).run(([0, 0, 0, 0], 9000)).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -2,6 +2,7 @@
|
||||||
"name": "knotes-frontend",
|
"name": "knotes-frontend",
|
||||||
"private": true,
|
"private": true,
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
"packageManager": "pnpm@10.6.3",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"start": "vite --port 9000",
|
"start": "vite --port 9000",
|
||||||
"build": "vite build && tsc",
|
"build": "vite build && tsc",
|
||||||
|
|
|
@ -13,6 +13,7 @@ import { mdiPin, mdiPinOff, mdiPlus, mdiTrashCan } from "@mdi/js";
|
||||||
import { createCollectionProperty, deleteCollectionProperty } from "~/lib/property";
|
import { createCollectionProperty, deleteCollectionProperty } from "~/lib/property";
|
||||||
import { useEffect } from "react";
|
import { useEffect } from "react";
|
||||||
import { PropertyTypeCombobox } from "../form/property_type_combobox";
|
import { PropertyTypeCombobox } from "../form/property_type_combobox";
|
||||||
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
|
||||||
type CollectionSettingsDialogProps = {
|
type CollectionSettingsDialogProps = {
|
||||||
name?: string;
|
name?: string;
|
||||||
|
@ -78,18 +79,18 @@ function SettingsSection(props: { collectionId: CollectionId }) {
|
||||||
function PropertiesSection(props: { collectionId: CollectionId }) {
|
function PropertiesSection(props: { collectionId: CollectionId }) {
|
||||||
const collection = useCollection(props.collectionId)
|
const collection = useCollection(props.collectionId)
|
||||||
|
|
||||||
if (!collection) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Ensure that the properties array exists on the collection
|
// Ensure that the properties array exists on the collection
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!collection.has("properties")) {
|
if (collection && !collection.has("properties")) {
|
||||||
collection.set("properties", new Y.Array() as any)
|
collection.set("properties", new Y.Array() as any)
|
||||||
}
|
}
|
||||||
}, [collection])
|
}, [collection])
|
||||||
|
|
||||||
const properties = collection.get("properties");
|
const properties = collection?.get("properties");
|
||||||
|
|
||||||
|
if (!collection) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -152,6 +153,7 @@ function PropertyItem(props: PropertyItemProps) {
|
||||||
|
|
||||||
function DangerSection(props: { collectionId: CollectionId }) {
|
function DangerSection(props: { collectionId: CollectionId }) {
|
||||||
const collections = useCollections();
|
const collections = useCollections();
|
||||||
|
const navigate = useNavigate();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
|
@ -182,8 +184,10 @@ function DangerSection(props: { collectionId: CollectionId }) {
|
||||||
<AlertDialogAction asChild>
|
<AlertDialogAction asChild>
|
||||||
<Button
|
<Button
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
// TODO: Navigate away from the page when clicked
|
onClick={() => {
|
||||||
onClick={() => deleteCollection(collections, props.collectionId)}
|
navigate({ to: "/app" });
|
||||||
|
deleteCollection(collections, props.collectionId);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Delete
|
Delete
|
||||||
</Button>
|
</Button>
|
||||||
|
|
|
@ -8,7 +8,7 @@ import type { ColorName } from "~/lib/color";
|
||||||
import { IconPicker } from "~/components/form/icon_picker";
|
import { IconPicker } from "~/components/form/icon_picker";
|
||||||
import type { IconName } from "~/lib/icon";
|
import type { IconName } from "~/lib/icon";
|
||||||
import { CollectionPicker } from "~/components/form/collection_picker";
|
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 { useNotesMetadata } from "~/hooks/use-metadata";
|
||||||
import { RadioGroup, RadioGroupItem } from "~/components/ui/radio-group";
|
import { RadioGroup, RadioGroupItem } from "~/components/ui/radio-group";
|
||||||
import { useNavigate } from "@tanstack/react-router";
|
import { useNavigate } from "@tanstack/react-router";
|
||||||
|
@ -25,7 +25,7 @@ export function NewNoteDialog(props: NewNoteDialogProps) {
|
||||||
const [collection, setCollection] = useState<CollectionId | undefined>();
|
const [collection, setCollection] = useState<CollectionId | undefined>();
|
||||||
const [primaryColor, setPrimaryColor] = useState<ColorName | undefined>();
|
const [primaryColor, setPrimaryColor] = useState<ColorName | undefined>();
|
||||||
const [secondaryColor, setSecondaryColor] = 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();
|
const navigate = useNavigate();
|
||||||
function submit() {
|
function submit() {
|
||||||
|
@ -102,6 +102,10 @@ export function NewNoteDialog(props: NewNoteDialogProps) {
|
||||||
<RadioGroupItem value="canvas" id="type-canvas" />
|
<RadioGroupItem value="canvas" id="type-canvas" />
|
||||||
<Label htmlFor="type-canvas">Canvas</Label>
|
<Label htmlFor="type-canvas">Canvas</Label>
|
||||||
</div>
|
</div>
|
||||||
|
<div className="flex items-center space-x-2">
|
||||||
|
<RadioGroupItem value="entity" id="type-entity" />
|
||||||
|
<Label htmlFor="type-entity">Entity</Label>
|
||||||
|
</div>
|
||||||
</RadioGroup>
|
</RadioGroup>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
|
@ -9,6 +9,8 @@ export type NoteId = string
|
||||||
export type CollectionId = string
|
export type CollectionId = string
|
||||||
export type PropertyId = string
|
export type PropertyId = string
|
||||||
|
|
||||||
|
export type NoteType = "text" | "canvas" | "entity"
|
||||||
|
|
||||||
export type NotesMetadata = YArray<NoteMetadata>
|
export type NotesMetadata = YArray<NoteMetadata>
|
||||||
|
|
||||||
export type NoteMetadata = YMap<{
|
export type NoteMetadata = YMap<{
|
||||||
|
@ -20,7 +22,7 @@ export type NoteMetadata = YMap<{
|
||||||
secondaryColor: ColorName | ""
|
secondaryColor: ColorName | ""
|
||||||
pinned: boolean
|
pinned: boolean
|
||||||
properties: YArray<NoteProperty>
|
properties: YArray<NoteProperty>
|
||||||
type: "text" | "canvas"
|
type: NoteType
|
||||||
}>
|
}>
|
||||||
|
|
||||||
export type NoteProperty = YMap<{
|
export type NoteProperty = YMap<{
|
||||||
|
@ -61,13 +63,13 @@ export function createCollection(md: CollectionsMetadata, data: {
|
||||||
return collection;
|
return collection;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteCollection(md: CollectionsMetadata, id: CollectionId) {
|
export function deleteCollection(cmd: CollectionsMetadata, id: CollectionId) {
|
||||||
const index = md.toArray().findIndex(it => it.get("id") == id);
|
const index = cmd.toArray().findIndex(it => it.get("id") == id);
|
||||||
if (index == -1) {
|
if (index == -1) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
md.delete(index, 1)
|
cmd.delete(index, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
export function createNote(md: NotesMetadata, data: {
|
export function createNote(md: NotesMetadata, data: {
|
||||||
|
@ -76,7 +78,7 @@ export function createNote(md: NotesMetadata, data: {
|
||||||
collectionId: CollectionId | undefined,
|
collectionId: CollectionId | undefined,
|
||||||
primaryColor: ColorName | undefined,
|
primaryColor: ColorName | undefined,
|
||||||
secondaryColor: ColorName | undefined,
|
secondaryColor: ColorName | undefined,
|
||||||
type: "text" | "canvas"
|
type: NoteType
|
||||||
}) {
|
}) {
|
||||||
const note = new Y.Map() as any as NoteMetadata;
|
const note = new Y.Map() as any as NoteMetadata;
|
||||||
note.set("id", randomUUID());
|
note.set("id", randomUUID());
|
||||||
|
|
|
@ -31,6 +31,9 @@ function Content(props: { id: string }) {
|
||||||
case "canvas": return <>
|
case "canvas": return <>
|
||||||
<NoteCanvas noteId={props.id} />
|
<NoteCanvas noteId={props.id} />
|
||||||
</>;
|
</>;
|
||||||
|
case "entity": return <>
|
||||||
|
Entity type
|
||||||
|
</>;
|
||||||
default: return null;
|
default: return null;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue