feat: initial working version of project manager
This commit is contained in:
@@ -109,6 +109,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
const serialized = {
|
const serialized = {
|
||||||
id: this.graph.id,
|
id: this.graph.id,
|
||||||
settings: $state.snapshot(this.settings),
|
settings: $state.snapshot(this.settings),
|
||||||
|
meta: this.graph.meta,
|
||||||
nodes,
|
nodes,
|
||||||
edges
|
edges
|
||||||
};
|
};
|
||||||
@@ -304,7 +305,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
this.status = 'loading';
|
this.status = 'loading';
|
||||||
this.id = graph.id;
|
this.id = graph.id;
|
||||||
|
|
||||||
logger.info('loading graph', $state.snapshot(graph));
|
logger.info('loading graph', graph);
|
||||||
|
|
||||||
const nodeIds = Array.from(new Set([...graph.nodes.map((n) => n.type)]));
|
const nodeIds = Array.from(new Set([...graph.nodes.map((n) => n.type)]));
|
||||||
await this.registry.load(nodeIds);
|
await this.registry.load(nodeIds);
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
import { setupKeymaps } from "../keymaps";
|
import { setupKeymaps } from "../keymaps";
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
graph: Graph;
|
graph?: Graph;
|
||||||
registry: NodeRegistry;
|
registry: NodeRegistry;
|
||||||
|
|
||||||
settings?: Record<string, any>;
|
settings?: Record<string, any>;
|
||||||
@@ -85,7 +85,11 @@
|
|||||||
|
|
||||||
manager.on("save", (save) => onsave?.(save));
|
manager.on("save", (save) => onsave?.(save));
|
||||||
|
|
||||||
|
$effect(() => {
|
||||||
|
if (graph) {
|
||||||
manager.load(graph);
|
manager.load(graph);
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<GraphEl {keymap} />
|
<GraphEl {keymap} />
|
||||||
|
|||||||
85
app/src/lib/project-manager/ProjectManager.svelte
Normal file
85
app/src/lib/project-manager/ProjectManager.svelte
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
<script lang="ts">
|
||||||
|
import type { Graph } from "$lib/types";
|
||||||
|
import { defaultPlant } from "$lib/graph-templates";
|
||||||
|
import type { ProjectManager } from "./project-manager.svelte";
|
||||||
|
|
||||||
|
const { projectManager } = $props<{ projectManager: ProjectManager }>();
|
||||||
|
|
||||||
|
let showNewProject = $state(false);
|
||||||
|
let newProjectName = $state("");
|
||||||
|
|
||||||
|
function handleCreate() {
|
||||||
|
projectManager.handleCreateProject(
|
||||||
|
defaultPlant as unknown as Graph,
|
||||||
|
newProjectName,
|
||||||
|
);
|
||||||
|
newProjectName = "";
|
||||||
|
showNewProject = false;
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<header
|
||||||
|
class="flex justify-between px-4 h-[70px] border-b-1 border-[var(--outline)] items-center"
|
||||||
|
>
|
||||||
|
<h3>Project</h3>
|
||||||
|
<button
|
||||||
|
class="px-3 py-1 bg-[var(--layer-0)] rounded"
|
||||||
|
onclick={() => (showNewProject = !showNewProject)}
|
||||||
|
>
|
||||||
|
New
|
||||||
|
</button>
|
||||||
|
</header>
|
||||||
|
|
||||||
|
{#if showNewProject}
|
||||||
|
<div
|
||||||
|
class="flex justify-between px-4 h-[70px] border-b-1 border-[var(--outline)] items-center gap-2"
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
bind:value={newProjectName}
|
||||||
|
placeholder="Project name"
|
||||||
|
class="flex-1 min-w-0 px-2 py-2 bg-gray-800 border border-gray-700 rounded"
|
||||||
|
onkeydown={(e) => e.key === "Enter" && handleCreate()}
|
||||||
|
/>
|
||||||
|
<button class="cursor-pointer" onclick={() => handleCreate()}>
|
||||||
|
Create
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<div class="p-4 text-white min-h-screen">
|
||||||
|
{#if projectManager.loading}
|
||||||
|
<p>Loading...</p>
|
||||||
|
{/if}
|
||||||
|
|
||||||
|
<ul class="space-y-2">
|
||||||
|
{#each projectManager.projects as project (project.id)}
|
||||||
|
<li>
|
||||||
|
<div
|
||||||
|
class="w-full text-left px-3 py-2 rounded cursor-pointer {projectManager
|
||||||
|
.activeProjectId.value === project.id
|
||||||
|
? 'bg-blue-600'
|
||||||
|
: 'bg-gray-800 hover:bg-gray-700'}"
|
||||||
|
onclick={() => projectManager.handleSelectProject(project.id!)}
|
||||||
|
role="button"
|
||||||
|
tabindex="0"
|
||||||
|
onkeydown={(e) =>
|
||||||
|
e.key === "Enter" &&
|
||||||
|
projectManager.handleSelectProject(project.id!)}
|
||||||
|
>
|
||||||
|
<div class="flex justify-between items-center">
|
||||||
|
<span>{project.meta?.title || "Untitled"}</span>
|
||||||
|
<button
|
||||||
|
class="text-red-400 hover:text-red-300"
|
||||||
|
onclick={() => {
|
||||||
|
projectManager.handleDeleteProject(project.id!);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
×
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
{/each}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
54
app/src/lib/project-manager/project-database.svelte.ts
Normal file
54
app/src/lib/project-manager/project-database.svelte.ts
Normal file
@@ -0,0 +1,54 @@
|
|||||||
|
import type { Graph } from '@nodarium/types';
|
||||||
|
import { type IDBPDatabase, openDB } from 'idb';
|
||||||
|
|
||||||
|
export interface GraphDatabase {
|
||||||
|
projects: Graph;
|
||||||
|
}
|
||||||
|
|
||||||
|
const DB_NAME = 'nodarium-graphs';
|
||||||
|
const DB_VERSION = 1;
|
||||||
|
const STORE_NAME = 'graphs';
|
||||||
|
|
||||||
|
let dbPromise: Promise<IDBPDatabase<GraphDatabase>> | null = null;
|
||||||
|
|
||||||
|
export function getDB() {
|
||||||
|
if (!dbPromise) {
|
||||||
|
dbPromise = openDB<GraphDatabase>(DB_NAME, DB_VERSION, {
|
||||||
|
upgrade(db) {
|
||||||
|
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||||
|
db.createObjectStore(STORE_NAME, { keyPath: 'id' });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return dbPromise;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getGraph(id: number): Promise<Graph | undefined> {
|
||||||
|
const db = await getDB();
|
||||||
|
return db.get(STORE_NAME, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function saveGraph(graph: Graph): Promise<Graph> {
|
||||||
|
const db = await getDB();
|
||||||
|
graph.meta = { ...graph.meta, lastModified: new Date().toISOString() };
|
||||||
|
console.log('SAVING GRAPH', { graph });
|
||||||
|
await db.put(STORE_NAME, graph);
|
||||||
|
return graph;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function deleteGraph(id: number): Promise<void> {
|
||||||
|
const db = await getDB();
|
||||||
|
await db.delete(STORE_NAME, id);
|
||||||
|
console.log('DELETE GRAPH', { id });
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function getGraphs(): Promise<Graph[]> {
|
||||||
|
const db = await getDB();
|
||||||
|
return db.getAll(STORE_NAME);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function clear(): Promise<void> {
|
||||||
|
const db = await getDB();
|
||||||
|
return db.clear(STORE_NAME);
|
||||||
|
}
|
||||||
93
app/src/lib/project-manager/project-manager.svelte.ts
Normal file
93
app/src/lib/project-manager/project-manager.svelte.ts
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
import * as templates from '$lib/graph-templates';
|
||||||
|
import { localState } from '$lib/helpers/localState.svelte';
|
||||||
|
import type { Graph } from '@nodarium/types';
|
||||||
|
import * as db from './project-database.svelte';
|
||||||
|
|
||||||
|
export class ProjectManager {
|
||||||
|
public graph = $state<Graph>();
|
||||||
|
private projects = $state<Graph[]>([]);
|
||||||
|
private activeProjectId = localState<number | undefined>(
|
||||||
|
'node.activeProjectId',
|
||||||
|
undefined
|
||||||
|
);
|
||||||
|
public readonly loading = $derived(this.graph?.id !== this.activeProjectId.value);
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.init();
|
||||||
|
}
|
||||||
|
|
||||||
|
async saveGraph(g: Graph) {
|
||||||
|
db.saveGraph(g);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async init() {
|
||||||
|
await db.getDB();
|
||||||
|
this.projects = await db.getGraphs();
|
||||||
|
|
||||||
|
console.log('PM: INIT', {
|
||||||
|
projects: this.projects,
|
||||||
|
activeProjectId: this.activeProjectId.value
|
||||||
|
});
|
||||||
|
|
||||||
|
if (this.activeProjectId.value !== undefined) {
|
||||||
|
let loadedGraph = await db.getGraph(this.activeProjectId.value);
|
||||||
|
console.log('PM: LOAD ACTIVE PROJECT', { loadedGraph });
|
||||||
|
if (loadedGraph) {
|
||||||
|
console.log('Load active project');
|
||||||
|
this.graph = loadedGraph;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.graph) {
|
||||||
|
console.log('Load first active project', { projectsAmount: this.projects.length });
|
||||||
|
if (this.projects?.length && this.projects[0]?.id !== undefined) {
|
||||||
|
this.graph = this.projects[0];
|
||||||
|
this.activeProjectId.value = this.graph.id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!this.graph) {
|
||||||
|
console.log('Create default project');
|
||||||
|
this.handleCreateProject();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public handleCreateProject(
|
||||||
|
g: Graph = templates.defaultPlant as unknown as Graph,
|
||||||
|
title: string = 'New Project'
|
||||||
|
) {
|
||||||
|
let id = g?.id || 0;
|
||||||
|
while (this.projects.find((p) => p.id === id)) {
|
||||||
|
id++;
|
||||||
|
}
|
||||||
|
|
||||||
|
console.log('CREATE PROJECT', { id, title });
|
||||||
|
|
||||||
|
g.id = id;
|
||||||
|
if (!g.meta) g.meta = {};
|
||||||
|
if (!g.meta.title) g.meta.title = title;
|
||||||
|
|
||||||
|
db.saveGraph(g);
|
||||||
|
this.projects = [...this.projects, g];
|
||||||
|
this.handleSelectProject(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async handleDeleteProject(projectId: number) {
|
||||||
|
await db.deleteGraph(projectId);
|
||||||
|
if (this.projects.length === 1) {
|
||||||
|
this.graph = undefined;
|
||||||
|
this.projects = [];
|
||||||
|
} else {
|
||||||
|
this.projects = this.projects.filter((p) => p.id !== projectId);
|
||||||
|
this.handleSelectProject(this.projects[0].id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public async handleSelectProject(id: number) {
|
||||||
|
if (this.activeProjectId.value !== id) {
|
||||||
|
const project = await db.getGraph(id);
|
||||||
|
this.graph = project;
|
||||||
|
this.activeProjectId.value = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -43,7 +43,4 @@
|
|||||||
align-items: center;
|
align-items: center;
|
||||||
padding-left: 1em;
|
padding-left: 1em;
|
||||||
}
|
}
|
||||||
h3 {
|
|
||||||
margin: 0px;
|
|
||||||
}
|
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import type { Graph } from "$lib/types";
|
import type { Graph } from "$lib/types";
|
||||||
|
|
||||||
const { graph }: { graph: Graph } = $props();
|
const { graph }: { graph?: Graph } = $props();
|
||||||
|
|
||||||
function convert(g: Graph): string {
|
function convert(g: Graph): string {
|
||||||
return JSON.stringify(
|
return JSON.stringify(
|
||||||
@@ -16,5 +16,5 @@
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<pre>
|
<pre>
|
||||||
{convert(graph)}
|
{graph ? convert(graph) : 'No graph loaded'}
|
||||||
</pre>
|
</pre>
|
||||||
|
|||||||
@@ -28,6 +28,8 @@
|
|||||||
import BenchmarkPanel from "$lib/sidebar/panels/BenchmarkPanel.svelte";
|
import BenchmarkPanel from "$lib/sidebar/panels/BenchmarkPanel.svelte";
|
||||||
import { debounceAsyncFunction } from "$lib/helpers";
|
import { debounceAsyncFunction } from "$lib/helpers";
|
||||||
import GraphSource from "$lib/sidebar/panels/GraphSource.svelte";
|
import GraphSource from "$lib/sidebar/panels/GraphSource.svelte";
|
||||||
|
import { ProjectManager } from "$lib/project-manager/project-manager.svelte";
|
||||||
|
import ProjectManagerEl from "$lib/project-manager/ProjectManager.svelte";
|
||||||
|
|
||||||
let performanceStore = createPerformanceStore();
|
let performanceStore = createPerformanceStore();
|
||||||
|
|
||||||
@@ -37,6 +39,7 @@
|
|||||||
const runtimeCache = new MemoryRuntimeCache();
|
const runtimeCache = new MemoryRuntimeCache();
|
||||||
const memoryRuntime = new MemoryRuntimeExecutor(nodeRegistry, runtimeCache);
|
const memoryRuntime = new MemoryRuntimeExecutor(nodeRegistry, runtimeCache);
|
||||||
memoryRuntime.perf = performanceStore;
|
memoryRuntime.perf = performanceStore;
|
||||||
|
const pm = new ProjectManager();
|
||||||
|
|
||||||
const runtime = $derived(
|
const runtime = $derived(
|
||||||
appSettings.value.debug.useWorker ? workerRuntime : memoryRuntime,
|
appSettings.value.debug.useWorker ? workerRuntime : memoryRuntime,
|
||||||
@@ -64,15 +67,6 @@
|
|||||||
let activeNode = $state<NodeInstance | undefined>(undefined);
|
let activeNode = $state<NodeInstance | undefined>(undefined);
|
||||||
let scene = $state<Group>(null!);
|
let scene = $state<Group>(null!);
|
||||||
|
|
||||||
let graph = $state(
|
|
||||||
localStorage.getItem("graph")
|
|
||||||
? JSON.parse(localStorage.getItem("graph")!)
|
|
||||||
: templates.defaultPlant,
|
|
||||||
);
|
|
||||||
function handleSave(graph: Graph) {
|
|
||||||
localStorage.setItem("graph", JSON.stringify(graph));
|
|
||||||
}
|
|
||||||
|
|
||||||
let graphInterface = $state<ReturnType<typeof GraphInterface>>(null!);
|
let graphInterface = $state<ReturnType<typeof GraphInterface>>(null!);
|
||||||
let viewerComponent = $state<ReturnType<typeof Viewer>>();
|
let viewerComponent = $state<ReturnType<typeof Viewer>>();
|
||||||
const manager = $derived(graphInterface?.manager);
|
const manager = $derived(graphInterface?.manager);
|
||||||
@@ -172,7 +166,7 @@
|
|||||||
<svelte:document onkeydown={applicationKeymap.handleKeyboardEvent} />
|
<svelte:document onkeydown={applicationKeymap.handleKeyboardEvent} />
|
||||||
|
|
||||||
<div class="wrapper manager-{manager?.status}">
|
<div class="wrapper manager-{manager?.status}">
|
||||||
<header class="h-[40px] border-b-1 border-[var(--outline)]"></header>
|
<header></header>
|
||||||
<Grid.Row>
|
<Grid.Row>
|
||||||
<Grid.Cell>
|
<Grid.Cell>
|
||||||
<Viewer
|
<Viewer
|
||||||
@@ -183,8 +177,9 @@
|
|||||||
/>
|
/>
|
||||||
</Grid.Cell>
|
</Grid.Cell>
|
||||||
<Grid.Cell>
|
<Grid.Cell>
|
||||||
|
{#if pm.graph}
|
||||||
<GraphInterface
|
<GraphInterface
|
||||||
{graph}
|
graph={pm.graph}
|
||||||
bind:this={graphInterface}
|
bind:this={graphInterface}
|
||||||
registry={nodeRegistry}
|
registry={nodeRegistry}
|
||||||
showGrid={appSettings.value.nodeInterface.showNodeGrid}
|
showGrid={appSettings.value.nodeInterface.showNodeGrid}
|
||||||
@@ -193,9 +188,10 @@
|
|||||||
bind:showHelp={appSettings.value.nodeInterface.showHelp}
|
bind:showHelp={appSettings.value.nodeInterface.showHelp}
|
||||||
bind:settings={graphSettings}
|
bind:settings={graphSettings}
|
||||||
bind:settingTypes={graphSettingTypes}
|
bind:settingTypes={graphSettingTypes}
|
||||||
|
onsave={(g) => pm.saveGraph(g)}
|
||||||
onresult={(result) => handleUpdate(result)}
|
onresult={(result) => handleUpdate(result)}
|
||||||
onsave={(graph) => handleSave(graph)}
|
|
||||||
/>
|
/>
|
||||||
|
{/if}
|
||||||
<Sidebar>
|
<Sidebar>
|
||||||
<Panel id="general" title="General" icon="i-[tabler--settings]">
|
<Panel id="general" title="General" icon="i-[tabler--settings]">
|
||||||
<NestedSettings
|
<NestedSettings
|
||||||
@@ -236,13 +232,16 @@
|
|||||||
<PerformanceViewer data={$performanceStore} />
|
<PerformanceViewer data={$performanceStore} />
|
||||||
{/if}
|
{/if}
|
||||||
</Panel>
|
</Panel>
|
||||||
|
<Panel id="projects" icon="i-[tabler--folder-open]">
|
||||||
|
<ProjectManagerEl projectManager={pm} />
|
||||||
|
</Panel>
|
||||||
<Panel
|
<Panel
|
||||||
id="graph-source"
|
id="graph-source"
|
||||||
title="Graph Source"
|
title="Graph Source"
|
||||||
hidden={!appSettings.value.debug.showGraphJson}
|
hidden={!appSettings.value.debug.showGraphJson}
|
||||||
icon="i-[tabler--code]"
|
icon="i-[tabler--code]"
|
||||||
>
|
>
|
||||||
<GraphSource graph={graph && manager.serialize()} />
|
<GraphSource graph={pm.graph ?? manager?.serialize()} />
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel
|
<Panel
|
||||||
id="benchmark"
|
id="benchmark"
|
||||||
@@ -285,7 +284,7 @@
|
|||||||
width: 100vw;
|
width: 100vw;
|
||||||
color: white;
|
color: white;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-rows: 40px 1fr;
|
grid-template-rows: 0px 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
.wrapper :global(canvas) {
|
.wrapper :global(canvas) {
|
||||||
|
|||||||
@@ -77,7 +77,7 @@ export type Socket = {
|
|||||||
export type Edge = [NodeInstance, number, NodeInstance, string];
|
export type Edge = [NodeInstance, number, NodeInstance, string];
|
||||||
|
|
||||||
export const GraphSchema = z.object({
|
export const GraphSchema = z.object({
|
||||||
id: z.number(),
|
id: z.number().optional(),
|
||||||
meta: z
|
meta: z
|
||||||
.object({
|
.object({
|
||||||
title: z.string().optional(),
|
title: z.string().optional(),
|
||||||
|
|||||||
0
packages/ui/src/lib/inputs/Search.svelte
Normal file
0
packages/ui/src/lib/inputs/Search.svelte
Normal file
Reference in New Issue
Block a user