init: fuzzygraph app from fn_registry
This commit is contained in:
@@ -0,0 +1,6 @@
|
||||
frontend/node_modules/
|
||||
node_modules/
|
||||
fuzzygraph
|
||||
fuzzygraph.log
|
||||
build/bin/
|
||||
*.db
|
||||
@@ -0,0 +1,606 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
ops "fn-registry/fn_operations"
|
||||
"fn-registry/registry"
|
||||
)
|
||||
|
||||
// App is the Wails-bound application struct.
|
||||
type App struct {
|
||||
ctx context.Context
|
||||
mu sync.RWMutex
|
||||
projectsDir string
|
||||
registryRoot string
|
||||
registryDB *registry.DB
|
||||
db *ops.DB
|
||||
currentProj string
|
||||
}
|
||||
|
||||
// NewApp creates a new App instance.
|
||||
func NewApp(projectsDir, registryRoot string) *App {
|
||||
log.Printf("[NewApp] projectsDir=%s registryRoot=%s", projectsDir, registryRoot)
|
||||
return &App{
|
||||
projectsDir: projectsDir,
|
||||
registryRoot: registryRoot,
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) startup(ctx context.Context) {
|
||||
a.ctx = ctx
|
||||
log.Println("[startup] called")
|
||||
|
||||
// Open registry.db for type lookups and snapshots
|
||||
regPath := filepath.Join(a.registryRoot, "registry.db")
|
||||
log.Printf("[startup] opening registry.db at %s", regPath)
|
||||
if db, err := registry.Open(regPath); err == nil {
|
||||
a.registryDB = db
|
||||
log.Println("[startup] registry.db opened OK")
|
||||
} else {
|
||||
log.Printf("[startup] WARNING: registry.db failed: %v", err)
|
||||
}
|
||||
|
||||
// Ensure projects directory exists
|
||||
log.Printf("[startup] ensuring projectsDir exists: %s", a.projectsDir)
|
||||
if err := os.MkdirAll(a.projectsDir, 0o755); err != nil {
|
||||
log.Printf("[startup] ERROR creating projectsDir: %v", err)
|
||||
}
|
||||
|
||||
// List what's there already
|
||||
entries, err := os.ReadDir(a.projectsDir)
|
||||
if err != nil {
|
||||
log.Printf("[startup] ERROR reading projectsDir: %v", err)
|
||||
} else {
|
||||
log.Printf("[startup] projectsDir has %d entries", len(entries))
|
||||
for _, e := range entries {
|
||||
log.Printf("[startup] - %s (dir=%v)", e.Name(), e.IsDir())
|
||||
}
|
||||
}
|
||||
|
||||
log.Println("[startup] done")
|
||||
}
|
||||
|
||||
func (a *App) shutdown(_ context.Context) {
|
||||
log.Println("[shutdown] called")
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
if a.db != nil {
|
||||
a.db.Close()
|
||||
log.Println("[shutdown] closed project db")
|
||||
}
|
||||
if a.registryDB != nil {
|
||||
a.registryDB.Close()
|
||||
log.Println("[shutdown] closed registry db")
|
||||
}
|
||||
}
|
||||
|
||||
// --- Projects ---
|
||||
|
||||
func (a *App) ListProjects() ([]ProjectInfo, error) {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
log.Printf("[ListProjects] projectsDir=%s", a.projectsDir)
|
||||
projects, err := listProjectDirs(a.projectsDir)
|
||||
if err != nil {
|
||||
log.Printf("[ListProjects] ERROR: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
log.Printf("[ListProjects] found %d projects", len(projects))
|
||||
for _, p := range projects {
|
||||
log.Printf("[ListProjects] - %s (entities=%d relations=%d)", p.Name, p.EntityCount, p.RelCount)
|
||||
}
|
||||
return projects, nil
|
||||
}
|
||||
|
||||
func (a *App) CreateProject(name string) (ProjectInfo, error) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
name = strings.TrimSpace(name)
|
||||
log.Printf("[CreateProject] name=%q projectsDir=%s", name, a.projectsDir)
|
||||
|
||||
if name == "" {
|
||||
log.Println("[CreateProject] ERROR: empty name")
|
||||
return ProjectInfo{}, fmt.Errorf("project name cannot be empty")
|
||||
}
|
||||
|
||||
targetDir := filepath.Join(a.projectsDir, name)
|
||||
log.Printf("[CreateProject] targetDir=%s", targetDir)
|
||||
|
||||
if err := createProject(a.projectsDir, name); err != nil {
|
||||
log.Printf("[CreateProject] ERROR: %v", err)
|
||||
return ProjectInfo{}, err
|
||||
}
|
||||
|
||||
// Verify it was created
|
||||
dbPath := filepath.Join(targetDir, "operations.db")
|
||||
if _, err := os.Stat(dbPath); err != nil {
|
||||
log.Printf("[CreateProject] WARNING: operations.db not found after create: %v", err)
|
||||
} else {
|
||||
log.Printf("[CreateProject] OK: operations.db exists at %s", dbPath)
|
||||
}
|
||||
|
||||
log.Printf("[CreateProject] success: %s", name)
|
||||
return ProjectInfo{Name: name}, nil
|
||||
}
|
||||
|
||||
func (a *App) SwitchProject(name string) error {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
log.Printf("[SwitchProject] name=%q (current=%q)", name, a.currentProj)
|
||||
|
||||
if a.db != nil {
|
||||
a.db.Close()
|
||||
a.db = nil
|
||||
log.Println("[SwitchProject] closed previous db")
|
||||
}
|
||||
|
||||
dbPath := filepath.Join(a.projectsDir, name, "operations.db")
|
||||
log.Printf("[SwitchProject] opening %s", dbPath)
|
||||
|
||||
db, err := ops.Open(dbPath)
|
||||
if err != nil {
|
||||
log.Printf("[SwitchProject] ERROR: %v", err)
|
||||
return fmt.Errorf("opening project %s: %w", name, err)
|
||||
}
|
||||
|
||||
a.db = db
|
||||
a.currentProj = name
|
||||
log.Printf("[SwitchProject] OK: now on project %s", name)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) DeleteProject(name string) error {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
|
||||
log.Printf("[DeleteProject] name=%q", name)
|
||||
|
||||
if a.currentProj == name && a.db != nil {
|
||||
a.db.Close()
|
||||
a.db = nil
|
||||
a.currentProj = ""
|
||||
log.Println("[DeleteProject] closed active project db")
|
||||
}
|
||||
|
||||
err := deleteProject(a.projectsDir, name)
|
||||
if err != nil {
|
||||
log.Printf("[DeleteProject] ERROR: %v", err)
|
||||
} else {
|
||||
log.Printf("[DeleteProject] OK: deleted %s", name)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (a *App) GetCurrentProject() string {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
log.Printf("[GetCurrentProject] -> %q", a.currentProj)
|
||||
return a.currentProj
|
||||
}
|
||||
|
||||
// --- Entities ---
|
||||
|
||||
func (a *App) ListEntities() ([]ops.Entity, error) {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
if a.db == nil {
|
||||
log.Println("[ListEntities] ERROR: no project selected")
|
||||
return nil, fmt.Errorf("no project selected")
|
||||
}
|
||||
entities, err := a.db.ListEntities("", "")
|
||||
if err != nil {
|
||||
log.Printf("[ListEntities] ERROR: %v", err)
|
||||
} else {
|
||||
log.Printf("[ListEntities] found %d entities", len(entities))
|
||||
}
|
||||
return entities, err
|
||||
}
|
||||
|
||||
func (a *App) GetEntity(id string) (*ops.Entity, error) {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
if a.db == nil {
|
||||
return nil, fmt.Errorf("no project selected")
|
||||
}
|
||||
log.Printf("[GetEntity] id=%s", id)
|
||||
return a.db.GetEntity(id)
|
||||
}
|
||||
|
||||
func (a *App) AddEntity(input EntityInput) (string, error) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
if a.db == nil {
|
||||
log.Println("[AddEntity] ERROR: no project selected")
|
||||
return "", fmt.Errorf("no project selected")
|
||||
}
|
||||
|
||||
id := makeEntityID(input.Name, input.TypeRef)
|
||||
log.Printf("[AddEntity] name=%q typeRef=%q -> id=%s", input.Name, input.TypeRef, id)
|
||||
now := time.Now()
|
||||
|
||||
e := &ops.Entity{
|
||||
ID: id,
|
||||
Name: input.Name,
|
||||
TypeRef: input.TypeRef,
|
||||
Status: ops.StatusActive,
|
||||
Description: input.Description,
|
||||
Domain: "osint",
|
||||
Tags: input.Tags,
|
||||
Source: "fuzzygraph",
|
||||
Metadata: input.Metadata,
|
||||
Notes: input.Notes,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
// Use InsertEntityWithSnapshot if registry is available
|
||||
if a.registryDB != nil {
|
||||
log.Println("[AddEntity] using InsertEntityWithSnapshot")
|
||||
if err := ops.InsertEntityWithSnapshot(a.db, a.registryDB, e); err != nil {
|
||||
log.Printf("[AddEntity] ERROR: %v", err)
|
||||
return "", err
|
||||
}
|
||||
} else {
|
||||
log.Println("[AddEntity] using plain InsertEntity (no registry)")
|
||||
if err := a.db.InsertEntity(e); err != nil {
|
||||
log.Printf("[AddEntity] ERROR: %v", err)
|
||||
return "", err
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("[AddEntity] OK: %s", id)
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (a *App) UpdateEntity(id string, input EntityInput) error {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
if a.db == nil {
|
||||
return fmt.Errorf("no project selected")
|
||||
}
|
||||
|
||||
log.Printf("[UpdateEntity] id=%s", id)
|
||||
|
||||
existing, err := a.db.GetEntity(id)
|
||||
if err != nil {
|
||||
log.Printf("[UpdateEntity] ERROR getting: %v", err)
|
||||
return err
|
||||
}
|
||||
if existing == nil {
|
||||
log.Printf("[UpdateEntity] ERROR: not found")
|
||||
return fmt.Errorf("entity %s not found", id)
|
||||
}
|
||||
|
||||
existing.Name = input.Name
|
||||
existing.TypeRef = input.TypeRef
|
||||
existing.Description = input.Description
|
||||
existing.Tags = input.Tags
|
||||
existing.Metadata = input.Metadata
|
||||
existing.Notes = input.Notes
|
||||
existing.UpdatedAt = time.Now()
|
||||
|
||||
if err := a.db.UpdateEntity(existing); err != nil {
|
||||
log.Printf("[UpdateEntity] ERROR: %v", err)
|
||||
return err
|
||||
}
|
||||
log.Printf("[UpdateEntity] OK: %s", id)
|
||||
return nil
|
||||
}
|
||||
|
||||
func (a *App) DeleteEntity(id string) error {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
if a.db == nil {
|
||||
return fmt.Errorf("no project selected")
|
||||
}
|
||||
log.Printf("[DeleteEntity] id=%s", id)
|
||||
return a.db.DeleteEntity(id)
|
||||
}
|
||||
|
||||
// --- Relations ---
|
||||
|
||||
func (a *App) ListRelations() ([]ops.Relation, error) {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
if a.db == nil {
|
||||
log.Println("[ListRelations] ERROR: no project selected")
|
||||
return nil, fmt.Errorf("no project selected")
|
||||
}
|
||||
relations, err := a.db.ListRelations("")
|
||||
if err != nil {
|
||||
log.Printf("[ListRelations] ERROR: %v", err)
|
||||
} else {
|
||||
log.Printf("[ListRelations] found %d relations", len(relations))
|
||||
}
|
||||
return relations, err
|
||||
}
|
||||
|
||||
func (a *App) AddRelation(input RelationInputDTO) (string, error) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
if a.db == nil {
|
||||
return "", fmt.Errorf("no project selected")
|
||||
}
|
||||
|
||||
id := generateID()
|
||||
log.Printf("[AddRelation] name=%q from=%s to=%s id=%s", input.Name, input.FromEntity, input.ToEntity, id)
|
||||
now := time.Now()
|
||||
|
||||
r := &ops.Relation{
|
||||
ID: id,
|
||||
Name: input.Name,
|
||||
FromEntity: input.FromEntity,
|
||||
ToEntity: input.ToEntity,
|
||||
Description: input.Description,
|
||||
Purity: "impure",
|
||||
Direction: ops.DirUnidirectional,
|
||||
Weight: input.Weight,
|
||||
Status: ops.RelImplemented,
|
||||
Tags: input.Tags,
|
||||
Notes: input.Notes,
|
||||
CreatedAt: now,
|
||||
UpdatedAt: now,
|
||||
}
|
||||
|
||||
if err := a.db.InsertRelation(r); err != nil {
|
||||
log.Printf("[AddRelation] ERROR: %v", err)
|
||||
return "", err
|
||||
}
|
||||
|
||||
log.Printf("[AddRelation] OK: %s", id)
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (a *App) UpdateRelation(id string, input RelationInputDTO) error {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
if a.db == nil {
|
||||
return fmt.Errorf("no project selected")
|
||||
}
|
||||
|
||||
log.Printf("[UpdateRelation] id=%s", id)
|
||||
|
||||
existing, err := a.db.GetRelation(id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if existing == nil {
|
||||
return fmt.Errorf("relation %s not found", id)
|
||||
}
|
||||
|
||||
existing.Name = input.Name
|
||||
existing.FromEntity = input.FromEntity
|
||||
existing.ToEntity = input.ToEntity
|
||||
existing.Description = input.Description
|
||||
existing.Weight = input.Weight
|
||||
existing.Tags = input.Tags
|
||||
existing.Notes = input.Notes
|
||||
existing.UpdatedAt = time.Now()
|
||||
|
||||
return a.db.UpdateRelation(existing)
|
||||
}
|
||||
|
||||
func (a *App) DeleteRelation(id string) error {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
if a.db == nil {
|
||||
return fmt.Errorf("no project selected")
|
||||
}
|
||||
log.Printf("[DeleteRelation] id=%s", id)
|
||||
return a.db.DeleteRelation(id)
|
||||
}
|
||||
|
||||
// --- Graph ---
|
||||
|
||||
func (a *App) GetGraphData() (GraphData, error) {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
if a.db == nil {
|
||||
log.Println("[GetGraphData] no project selected")
|
||||
return GraphData{}, fmt.Errorf("no project selected")
|
||||
}
|
||||
data, err := buildGraphData(a.db)
|
||||
if err != nil {
|
||||
log.Printf("[GetGraphData] ERROR: %v", err)
|
||||
} else {
|
||||
log.Printf("[GetGraphData] nodes=%d edges=%d", len(data.Nodes), len(data.Edges))
|
||||
}
|
||||
return data, err
|
||||
}
|
||||
|
||||
func (a *App) GetEntityNeighbors(entityID string, depth int) (GraphData, error) {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
if a.db == nil {
|
||||
return GraphData{}, fmt.Errorf("no project selected")
|
||||
}
|
||||
log.Printf("[GetEntityNeighbors] entityID=%s depth=%d", entityID, depth)
|
||||
return buildEgoGraph(a.db, entityID, depth)
|
||||
}
|
||||
|
||||
func (a *App) GetFilteredGraph(typeFilters []string) (GraphData, error) {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
if a.db == nil {
|
||||
return GraphData{}, fmt.Errorf("no project selected")
|
||||
}
|
||||
|
||||
log.Printf("[GetFilteredGraph] filters=%v", typeFilters)
|
||||
|
||||
full, err := buildGraphData(a.db)
|
||||
if err != nil {
|
||||
return GraphData{}, err
|
||||
}
|
||||
|
||||
if len(typeFilters) == 0 {
|
||||
return full, nil
|
||||
}
|
||||
|
||||
allowed := map[string]bool{}
|
||||
for _, t := range typeFilters {
|
||||
allowed[t] = true
|
||||
}
|
||||
|
||||
nodeIDs := map[string]bool{}
|
||||
var nodes []GraphNode
|
||||
for _, n := range full.Nodes {
|
||||
if allowed[n.Type] {
|
||||
nodes = append(nodes, n)
|
||||
nodeIDs[n.ID] = true
|
||||
}
|
||||
}
|
||||
|
||||
var edges []GraphEdge
|
||||
for _, e := range full.Edges {
|
||||
if nodeIDs[e.Source] && nodeIDs[e.Target] {
|
||||
edges = append(edges, e)
|
||||
}
|
||||
}
|
||||
|
||||
return GraphData{Nodes: nodes, Edges: edges}, nil
|
||||
}
|
||||
|
||||
// --- Search ---
|
||||
|
||||
func (a *App) SearchEntities(query string) ([]ops.Entity, error) {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
if a.db == nil {
|
||||
return nil, fmt.Errorf("no project selected")
|
||||
}
|
||||
log.Printf("[SearchEntities] query=%q", query)
|
||||
results, err := searchEntitiesFTS(a.db, query)
|
||||
if err != nil {
|
||||
log.Printf("[SearchEntities] ERROR: %v", err)
|
||||
} else {
|
||||
log.Printf("[SearchEntities] found %d results", len(results))
|
||||
}
|
||||
return results, err
|
||||
}
|
||||
|
||||
func (a *App) SearchGraph(query string) (GraphData, error) {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
if a.db == nil {
|
||||
return GraphData{}, fmt.Errorf("no project selected")
|
||||
}
|
||||
log.Printf("[SearchGraph] query=%q", query)
|
||||
return searchGraph(a.db, query)
|
||||
}
|
||||
|
||||
// --- Assertions ---
|
||||
|
||||
func (a *App) ListAssertions(entityID string) ([]ops.Assertion, error) {
|
||||
a.mu.RLock()
|
||||
defer a.mu.RUnlock()
|
||||
if a.db == nil {
|
||||
return nil, fmt.Errorf("no project selected")
|
||||
}
|
||||
log.Printf("[ListAssertions] entityID=%q", entityID)
|
||||
active := true
|
||||
return a.db.ListAssertions(entityID, &active)
|
||||
}
|
||||
|
||||
func (a *App) AddAssertion(input AssertionInput) (string, error) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
if a.db == nil {
|
||||
return "", fmt.Errorf("no project selected")
|
||||
}
|
||||
|
||||
id := generateID()
|
||||
log.Printf("[AddAssertion] name=%q entityID=%s id=%s", input.Name, input.EntityID, id)
|
||||
|
||||
assertion := &ops.Assertion{
|
||||
ID: id,
|
||||
EntityID: input.EntityID,
|
||||
Name: input.Name,
|
||||
Kind: input.Kind,
|
||||
Rule: input.Rule,
|
||||
Severity: ops.Severity(input.Severity),
|
||||
Description: input.Description,
|
||||
Active: true,
|
||||
CreatedAt: time.Now(),
|
||||
}
|
||||
|
||||
if err := a.db.InsertAssertion(assertion); err != nil {
|
||||
log.Printf("[AddAssertion] ERROR: %v", err)
|
||||
return "", err
|
||||
}
|
||||
log.Printf("[AddAssertion] OK: %s", id)
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func (a *App) DeleteAssertion(id string) error {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
if a.db == nil {
|
||||
return fmt.Errorf("no project selected")
|
||||
}
|
||||
log.Printf("[DeleteAssertion] id=%s", id)
|
||||
return a.db.DeleteAssertion(id)
|
||||
}
|
||||
|
||||
func (a *App) EvalAssertions(entityID string) ([]ops.AssertionResult, error) {
|
||||
a.mu.Lock()
|
||||
defer a.mu.Unlock()
|
||||
if a.db == nil {
|
||||
return nil, fmt.Errorf("no project selected")
|
||||
}
|
||||
log.Printf("[EvalAssertions] entityID=%s", entityID)
|
||||
results, err := ops.EvalEntityAssertions(a.db, entityID, "")
|
||||
if err != nil {
|
||||
log.Printf("[EvalAssertions] ERROR: %v", err)
|
||||
} else {
|
||||
log.Printf("[EvalAssertions] %d results", len(results))
|
||||
}
|
||||
return results, err
|
||||
}
|
||||
|
||||
// --- Presets ---
|
||||
|
||||
func (a *App) GetEntityPresets() []EntityTypePreset {
|
||||
log.Printf("[GetEntityPresets] returning %d presets", len(entityTypePresets))
|
||||
return entityTypePresets
|
||||
}
|
||||
|
||||
func (a *App) GetRelationPresets() []string {
|
||||
log.Printf("[GetRelationPresets] returning %d presets", len(relationPresets))
|
||||
return relationPresets
|
||||
}
|
||||
|
||||
// --- Helpers ---
|
||||
|
||||
func makeEntityID(name, typeRef string) string {
|
||||
clean := strings.ToLower(strings.TrimSpace(name))
|
||||
clean = strings.ReplaceAll(clean, " ", "_")
|
||||
clean = strings.ReplaceAll(clean, "-", "_")
|
||||
|
||||
parts := strings.Split(typeRef, "_")
|
||||
shortType := typeRef
|
||||
if len(parts) >= 2 {
|
||||
shortType = parts[1]
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%s_%s", clean, shortType)
|
||||
}
|
||||
|
||||
func generateID() string {
|
||||
b := make([]byte, 16)
|
||||
rand.Read(b)
|
||||
b[6] = (b[6] & 0x0f) | 0x40
|
||||
b[8] = (b[8] & 0x3f) | 0x80
|
||||
return fmt.Sprintf("%08x-%04x-%04x-%04x-%012x",
|
||||
b[0:4], b[4:6], b[6:8], b[8:10], b[10:16])
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
#!/usr/bin/env bash
|
||||
# Launch fuzzygraph in dev mode with FTS5 support
|
||||
cd "$(dirname "$0")"
|
||||
CGO_ENABLED=1 wails dev -tags fts5
|
||||
+320
File diff suppressed because one or more lines are too long
+2
File diff suppressed because one or more lines are too long
Vendored
+13
@@ -0,0 +1,13 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>FuzzyGraph</title>
|
||||
<script type="module" crossorigin src="/assets/index-CYqMr7xa.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-Cjyz0t73.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,12 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>FuzzyGraph</title>
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script type="module" src="/src/main.tsx"></script>
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"name": "fuzzygraph",
|
||||
"private": true,
|
||||
"version": "0.0.1",
|
||||
"type": "module",
|
||||
"scripts": {
|
||||
"dev": "vite --host",
|
||||
"build": "tsc -b && vite build",
|
||||
"preview": "vite preview --host"
|
||||
},
|
||||
"dependencies": {
|
||||
"@base-ui/react": "^1.3.0",
|
||||
"@phosphor-icons/react": "^2.1.10",
|
||||
"@radix-ui/react-checkbox": "^1.3.3",
|
||||
"@radix-ui/react-slider": "^1.3.6",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"class-variance-authority": "^0.7.1",
|
||||
"clsx": "^2.1.1",
|
||||
"lucide-react": "^0.577.0",
|
||||
"react": "^19.2.4",
|
||||
"react-dom": "^19.2.4",
|
||||
"tailwind-merge": "^3.5.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@tailwindcss/vite": "^4.2.2",
|
||||
"@types/react": "^19.2.14",
|
||||
"@types/react-dom": "^19.2.3",
|
||||
"@vitejs/plugin-react": "^6.0.0",
|
||||
"tailwindcss": "^4.2.2",
|
||||
"typescript": "~5.9.3",
|
||||
"vite": "^8.0.0"
|
||||
}
|
||||
}
|
||||
Executable
+1
@@ -0,0 +1 @@
|
||||
925e378ac695fa8339fd9576604d1d9f
|
||||
Generated
+1289
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,215 @@
|
||||
import { useEffect, useState, useCallback } from 'react'
|
||||
import type { ProjectInfo, Entity, Relation, GraphData, EntityTypePreset } from './types'
|
||||
import { ProjectSidebar } from './components/ProjectSidebar'
|
||||
import { SearchBar } from '@fn_library'
|
||||
import { GraphView } from './components/GraphView'
|
||||
import { EntityTable } from './components/EntityTable'
|
||||
import { RelationTable } from './components/RelationTable'
|
||||
import { EntityDetail } from './components/EntityDetail'
|
||||
import { AssertionPanel } from './components/AssertionPanel'
|
||||
import { Tabs, TabsList, TabsTrigger, TabsContent } from '@fn_library'
|
||||
import * as WailsApp from './wailsjs/go/main/App'
|
||||
|
||||
export default function App() {
|
||||
const [projects, setProjects] = useState<ProjectInfo[]>([])
|
||||
const [currentProject, setCurrentProject] = useState<string>('')
|
||||
const [entities, setEntities] = useState<Entity[]>([])
|
||||
const [relations, setRelations] = useState<Relation[]>([])
|
||||
const [graphData, setGraphData] = useState<GraphData>({ nodes: [], edges: [] })
|
||||
const [presets, setPresets] = useState<EntityTypePreset[]>([])
|
||||
const [relationPresets, setRelationPresets] = useState<string[]>([])
|
||||
const [activeTab, setActiveTab] = useState('graph')
|
||||
const [selectedEntityId, setSelectedEntityId] = useState<string | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
console.log('[App] mount — loading presets and projects')
|
||||
refreshProjects()
|
||||
WailsApp.GetEntityPresets()
|
||||
.then(p => { console.log('[App] GetEntityPresets OK:', p?.length, 'presets'); setPresets(p as unknown as EntityTypePreset[]) })
|
||||
.catch(e => console.error('[App] GetEntityPresets ERROR:', e))
|
||||
WailsApp.GetRelationPresets()
|
||||
.then(p => { console.log('[App] GetRelationPresets OK:', p?.length); setRelationPresets(p) })
|
||||
.catch(e => console.error('[App] GetRelationPresets ERROR:', e))
|
||||
}, [])
|
||||
|
||||
const refreshProjects = useCallback(() => {
|
||||
console.log('[App] refreshProjects called')
|
||||
WailsApp.ListProjects()
|
||||
.then(p => {
|
||||
const list = (p || []) as unknown as ProjectInfo[]
|
||||
console.log('[App] ListProjects OK:', list.length, 'projects', JSON.stringify(list))
|
||||
setProjects(list)
|
||||
})
|
||||
.catch(e => console.error('[App] ListProjects ERROR:', e))
|
||||
}, [])
|
||||
|
||||
const refreshData = useCallback(() => {
|
||||
console.log('[App] refreshData called')
|
||||
WailsApp.ListEntities()
|
||||
.then(e => { const list = (e || []) as unknown as Entity[]; console.log('[App] ListEntities OK:', list.length); setEntities(list) })
|
||||
.catch(e => console.error('[App] ListEntities ERROR:', e))
|
||||
WailsApp.ListRelations()
|
||||
.then(r => { const list = (r || []) as unknown as Relation[]; console.log('[App] ListRelations OK:', list.length); setRelations(list) })
|
||||
.catch(e => console.error('[App] ListRelations ERROR:', e))
|
||||
WailsApp.GetGraphData()
|
||||
.then(g => { const data = (g || { nodes: [], edges: [] }) as unknown as GraphData; console.log('[App] GetGraphData OK: nodes=', data.nodes?.length, 'edges=', data.edges?.length); setGraphData(data) })
|
||||
.catch(e => console.error('[App] GetGraphData ERROR:', e))
|
||||
}, [])
|
||||
|
||||
const handleSwitchProject = useCallback(async (name: string) => {
|
||||
console.log('[App] handleSwitchProject:', name)
|
||||
try {
|
||||
await WailsApp.SwitchProject(name)
|
||||
console.log('[App] SwitchProject OK')
|
||||
setCurrentProject(name)
|
||||
refreshData()
|
||||
} catch (e) {
|
||||
console.error('[App] SwitchProject ERROR:', e)
|
||||
}
|
||||
}, [refreshData])
|
||||
|
||||
const handleCreateProject = useCallback(async (name: string) => {
|
||||
console.log('[App] handleCreateProject:', name)
|
||||
try {
|
||||
const result = await WailsApp.CreateProject(name)
|
||||
console.log('[App] CreateProject OK:', JSON.stringify(result))
|
||||
refreshProjects()
|
||||
console.log('[App] switching to new project...')
|
||||
await handleSwitchProject(name)
|
||||
console.log('[App] switched OK')
|
||||
} catch (e) {
|
||||
console.error('[App] CreateProject ERROR:', e)
|
||||
}
|
||||
}, [refreshProjects, handleSwitchProject])
|
||||
|
||||
const handleDeleteProject = useCallback(async (name: string) => {
|
||||
console.log('[App] handleDeleteProject:', name)
|
||||
try {
|
||||
await WailsApp.DeleteProject(name)
|
||||
console.log('[App] DeleteProject OK')
|
||||
if (currentProject === name) {
|
||||
setCurrentProject('')
|
||||
setEntities([])
|
||||
setRelations([])
|
||||
setGraphData({ nodes: [], edges: [] })
|
||||
}
|
||||
refreshProjects()
|
||||
} catch (e) {
|
||||
console.error('[App] DeleteProject ERROR:', e)
|
||||
}
|
||||
}, [currentProject, refreshProjects])
|
||||
|
||||
const handleSearch = useCallback(async (query: string) => {
|
||||
console.log('[App] handleSearch:', query)
|
||||
if (!query.trim()) {
|
||||
refreshData()
|
||||
return
|
||||
}
|
||||
try {
|
||||
const [ents, graph] = await Promise.all([
|
||||
WailsApp.SearchEntities(query),
|
||||
WailsApp.SearchGraph(query),
|
||||
])
|
||||
console.log('[App] Search OK: entities=', (ents as unknown[])?.length, 'graph nodes=', (graph as unknown as GraphData)?.nodes?.length)
|
||||
setEntities((ents || []) as unknown as Entity[])
|
||||
setGraphData((graph || { nodes: [], edges: [] }) as unknown as GraphData)
|
||||
} catch (e) {
|
||||
console.error('[App] Search ERROR:', e)
|
||||
}
|
||||
}, [refreshData])
|
||||
|
||||
const handleNodeClick = useCallback((nodeId: string) => {
|
||||
console.log('[App] handleNodeClick:', nodeId)
|
||||
setSelectedEntityId(nodeId)
|
||||
}, [])
|
||||
|
||||
const handleNodeDoubleClick = useCallback(async (nodeId: string) => {
|
||||
console.log('[App] handleNodeDoubleClick:', nodeId)
|
||||
try {
|
||||
const ego = await WailsApp.GetEntityNeighbors(nodeId, 2)
|
||||
setGraphData((ego || { nodes: [], edges: [] }) as unknown as GraphData)
|
||||
} catch (e) {
|
||||
console.error('[App] GetEntityNeighbors ERROR:', e)
|
||||
}
|
||||
}, [])
|
||||
|
||||
const selectedEntity = entities.find(e => e.id === selectedEntityId) ?? null
|
||||
|
||||
console.log('[App] render: projects=', projects.length, 'currentProject=', currentProject, 'entities=', entities.length, 'relations=', relations.length)
|
||||
|
||||
return (
|
||||
<div className="flex h-screen overflow-hidden">
|
||||
<ProjectSidebar
|
||||
projects={projects}
|
||||
current={currentProject}
|
||||
onSwitch={handleSwitchProject}
|
||||
onCreate={handleCreateProject}
|
||||
onDelete={handleDeleteProject}
|
||||
/>
|
||||
|
||||
<main className="flex-1 flex flex-col overflow-hidden">
|
||||
{currentProject ? (
|
||||
<>
|
||||
<div className="px-4 pt-3 pb-2 flex items-center gap-3 border-b" style={{ borderColor: 'var(--border)' }}>
|
||||
<h2 className="text-sm font-semibold" style={{ color: 'var(--foreground)' }}>{currentProject}</h2>
|
||||
<SearchBar onSearch={handleSearch} />
|
||||
</div>
|
||||
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="flex-1 flex flex-col overflow-hidden">
|
||||
<TabsList className="mx-4 mt-2">
|
||||
<TabsTrigger value="graph">Graph</TabsTrigger>
|
||||
<TabsTrigger value="entities">Entities ({entities.length})</TabsTrigger>
|
||||
<TabsTrigger value="relations">Relations ({relations.length})</TabsTrigger>
|
||||
<TabsTrigger value="assertions">Assertions</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<TabsContent value="graph" className="flex-1 flex overflow-hidden m-0 p-0">
|
||||
<div className="flex-1 relative">
|
||||
<GraphView
|
||||
data={graphData}
|
||||
presets={presets}
|
||||
onNodeClick={handleNodeClick}
|
||||
onNodeDoubleClick={handleNodeDoubleClick}
|
||||
/>
|
||||
</div>
|
||||
{selectedEntity && (
|
||||
<EntityDetail
|
||||
entity={selectedEntity}
|
||||
relations={relations}
|
||||
onClose={() => setSelectedEntityId(null)}
|
||||
onUpdate={refreshData}
|
||||
/>
|
||||
)}
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="entities" className="flex-1 overflow-auto px-4 pb-4 m-0">
|
||||
<EntityTable
|
||||
entities={entities}
|
||||
presets={presets}
|
||||
onRefresh={refreshData}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="relations" className="flex-1 overflow-auto px-4 pb-4 m-0">
|
||||
<RelationTable
|
||||
relations={relations}
|
||||
entities={entities}
|
||||
relationPresets={relationPresets}
|
||||
onRefresh={refreshData}
|
||||
/>
|
||||
</TabsContent>
|
||||
|
||||
<TabsContent value="assertions" className="flex-1 overflow-auto px-4 pb-4 m-0">
|
||||
<AssertionPanel entities={entities} />
|
||||
</TabsContent>
|
||||
</Tabs>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex-1 flex items-center justify-center">
|
||||
<p style={{ color: 'var(--muted-foreground)' }}>Select or create a project to begin</p>
|
||||
</div>
|
||||
)}
|
||||
</main>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,39 @@
|
||||
@import "tailwindcss";
|
||||
|
||||
:root {
|
||||
--background: oklch(8% 0.015 260);
|
||||
--foreground: oklch(95% 0.01 260);
|
||||
--muted: oklch(18% 0.02 260);
|
||||
--muted-foreground: oklch(60% 0.02 260);
|
||||
--border: oklch(15% 0.01 260);
|
||||
--primary: oklch(65% 0.22 260);
|
||||
--primary-foreground: oklch(98% 0.01 260);
|
||||
--secondary: oklch(20% 0.02 260);
|
||||
--secondary-foreground: oklch(95% 0.01 260);
|
||||
--accent: oklch(18% 0.03 260);
|
||||
--accent-foreground: oklch(95% 0.01 260);
|
||||
--destructive: oklch(55% 0.22 25);
|
||||
--destructive-foreground: oklch(98% 0.01 260);
|
||||
--card: oklch(11% 0.015 260);
|
||||
--card-foreground: oklch(95% 0.01 260);
|
||||
--popover: oklch(12% 0.015 260);
|
||||
--popover-foreground: oklch(95% 0.01 260);
|
||||
--ring: oklch(65% 0.22 260);
|
||||
--input: oklch(22% 0.02 260);
|
||||
--radius: 0.5rem;
|
||||
--success: oklch(65% 0.2 145);
|
||||
--success-foreground: oklch(98% 0.01 145);
|
||||
}
|
||||
|
||||
body {
|
||||
background-color: var(--background);
|
||||
color: var(--foreground);
|
||||
font-family: 'Geist Variable', system-ui, -apple-system, sans-serif;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
}
|
||||
|
||||
[data-slot="card"] {
|
||||
border: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,170 @@
|
||||
import { useState } from 'react'
|
||||
import type { Entity, Assertion, AssertionResult, AssertionInput } from '../types'
|
||||
import { Plus, Play, Trash2 } from 'lucide-react'
|
||||
import { SimpleSelect } from '@fn_library'
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@fn_library'
|
||||
import { Button } from '@fn_library'
|
||||
import * as WailsApp from '../wailsjs/go/main/App'
|
||||
|
||||
interface Props {
|
||||
entities: Entity[]
|
||||
}
|
||||
|
||||
export function AssertionPanel({ entities }: Props) {
|
||||
const [assertions, setAssertions] = useState<Assertion[]>([])
|
||||
const [results, setResults] = useState<AssertionResult[]>([])
|
||||
const [selectedEntity, setSelectedEntity] = useState(entities[0]?.id ?? '')
|
||||
const [showAdd, setShowAdd] = useState(false)
|
||||
const [newName, setNewName] = useState('')
|
||||
const [newKind, setNewKind] = useState('range')
|
||||
const [newRule, setNewRule] = useState('')
|
||||
const [newSeverity, setNewSeverity] = useState('warning')
|
||||
|
||||
const loadAssertions = async (entityId: string) => {
|
||||
setSelectedEntity(entityId)
|
||||
const list = await WailsApp.ListAssertions(entityId) as unknown as Assertion[]
|
||||
setAssertions(list || [])
|
||||
setResults([])
|
||||
}
|
||||
|
||||
const handleAdd = async () => {
|
||||
if (!newName || !newRule) return
|
||||
const input: AssertionInput = {
|
||||
entity_id: selectedEntity,
|
||||
name: newName,
|
||||
kind: newKind,
|
||||
rule: newRule,
|
||||
severity: newSeverity,
|
||||
description: '',
|
||||
}
|
||||
await WailsApp.AddAssertion(input as never)
|
||||
setShowAdd(false)
|
||||
setNewName('')
|
||||
setNewRule('')
|
||||
loadAssertions(selectedEntity)
|
||||
}
|
||||
|
||||
const handleEval = async () => {
|
||||
const res = await WailsApp.EvalAssertions(selectedEntity) as unknown as AssertionResult[]
|
||||
setResults(res || [])
|
||||
}
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
await WailsApp.DeleteAssertion(id)
|
||||
loadAssertions(selectedEntity)
|
||||
}
|
||||
|
||||
const inputStyle = { background: 'var(--input)', color: 'var(--foreground)', border: '1px solid var(--border)' }
|
||||
|
||||
return (
|
||||
<Card className="mt-3">
|
||||
<CardHeader className="flex flex-row items-center justify-between py-3">
|
||||
<CardTitle className="text-base">Assertions</CardTitle>
|
||||
<div className="flex gap-2">
|
||||
<SimpleSelect
|
||||
value={selectedEntity}
|
||||
onValueChange={loadAssertions}
|
||||
placeholder="Select entity..."
|
||||
className="w-48"
|
||||
options={entities.map(e => ({ value: e.id, label: e.name }))}
|
||||
/>
|
||||
<Button size="sm" onClick={handleEval} disabled={!selectedEntity}>
|
||||
<Play size={14} className="mr-1" /> Eval
|
||||
</Button>
|
||||
<Button size="sm" variant="outline" onClick={() => setShowAdd(!showAdd)} disabled={!selectedEntity}>
|
||||
<Plus size={14} />
|
||||
</Button>
|
||||
</div>
|
||||
</CardHeader>
|
||||
|
||||
{showAdd && (
|
||||
<div className="px-4 pb-3 flex gap-2 items-end">
|
||||
<div className="flex-1">
|
||||
<label className="block text-xs mb-1" style={{ color: 'var(--muted-foreground)' }}>Name</label>
|
||||
<input value={newName} onChange={e => setNewName(e.target.value)} className="w-full px-2 py-1 rounded text-sm" style={inputStyle} />
|
||||
</div>
|
||||
<div className="w-24">
|
||||
<label className="block text-xs mb-1" style={{ color: 'var(--muted-foreground)' }}>Kind</label>
|
||||
<SimpleSelect
|
||||
value={newKind}
|
||||
onValueChange={setNewKind}
|
||||
options={[
|
||||
{ value: 'range', label: 'range' },
|
||||
{ value: 'null', label: 'null' },
|
||||
{ value: 'statistical', label: 'statistical' },
|
||||
{ value: 'consistency', label: 'consistency' },
|
||||
{ value: 'freshness', label: 'freshness' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="w-24">
|
||||
<label className="block text-xs mb-1" style={{ color: 'var(--muted-foreground)' }}>Severity</label>
|
||||
<SimpleSelect
|
||||
value={newSeverity}
|
||||
onValueChange={setNewSeverity}
|
||||
options={[
|
||||
{ value: 'critical', label: 'critical' },
|
||||
{ value: 'warning', label: 'warning' },
|
||||
{ value: 'info', label: 'info' },
|
||||
]}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<label className="block text-xs mb-1" style={{ color: 'var(--muted-foreground)' }}>Rule (SQL expr)</label>
|
||||
<input value={newRule} onChange={e => setNewRule(e.target.value)} className="w-full px-2 py-1 rounded text-sm" style={inputStyle} placeholder="risk_score > 70" />
|
||||
</div>
|
||||
<button onClick={handleAdd} className="px-3 py-1 rounded text-sm" style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}>Add</button>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<CardContent className="p-0">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b" style={{ borderColor: 'var(--border)' }}>
|
||||
<th className="text-left px-4 py-2 font-medium" style={{ color: 'var(--muted-foreground)' }}>Name</th>
|
||||
<th className="text-left px-4 py-2 font-medium" style={{ color: 'var(--muted-foreground)' }}>Kind</th>
|
||||
<th className="text-left px-4 py-2 font-medium" style={{ color: 'var(--muted-foreground)' }}>Rule</th>
|
||||
<th className="text-left px-4 py-2 font-medium" style={{ color: 'var(--muted-foreground)' }}>Severity</th>
|
||||
<th className="text-left px-4 py-2 font-medium" style={{ color: 'var(--muted-foreground)' }}>Result</th>
|
||||
<th className="text-right px-4 py-2 font-medium" style={{ color: 'var(--muted-foreground)' }}>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{assertions.map(a => {
|
||||
const result = results.find(r => r.assertion_id === a.id)
|
||||
return (
|
||||
<tr key={a.id} className="border-b" style={{ borderColor: 'var(--border)' }}>
|
||||
<td className="px-4 py-2">{a.name}</td>
|
||||
<td className="px-4 py-2" style={{ color: 'var(--muted-foreground)' }}>{a.kind}</td>
|
||||
<td className="px-4 py-2 font-mono text-xs">{a.rule}</td>
|
||||
<td className="px-4 py-2">
|
||||
<span style={{ color: a.severity === 'critical' ? 'var(--destructive)' : a.severity === 'warning' ? 'var(--chart-3, #f59e0b)' : 'var(--muted-foreground)' }}>
|
||||
{a.severity}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-2">
|
||||
{result ? (
|
||||
<span style={{ color: result.status === 'pass' ? 'var(--success)' : 'var(--destructive)' }}>
|
||||
{result.status}
|
||||
</span>
|
||||
) : '—'}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-right">
|
||||
<button onClick={() => handleDelete(a.id)} className="p-1 rounded" style={{ color: 'var(--destructive)' }}>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
{assertions.length === 0 && (
|
||||
<tr><td colSpan={6} className="px-4 py-8 text-center" style={{ color: 'var(--muted-foreground)' }}>
|
||||
{selectedEntity ? 'No assertions for this entity' : 'Select an entity to view assertions'}
|
||||
</td></tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,91 @@
|
||||
import type { Entity, Relation } from '../types'
|
||||
import { X, ExternalLink } from 'lucide-react'
|
||||
|
||||
interface Props {
|
||||
entity: Entity
|
||||
relations: Relation[]
|
||||
onClose: () => void
|
||||
onUpdate: () => void
|
||||
}
|
||||
|
||||
export function EntityDetail({ entity, relations, onClose }: Props) {
|
||||
const directRelations = relations.filter(r => r.from_entity === entity.id || r.to_entity === entity.id)
|
||||
|
||||
return (
|
||||
<aside className="w-72 border-l overflow-y-auto" style={{ borderColor: 'var(--border)', background: 'var(--card)' }}>
|
||||
<div className="p-3 flex items-center justify-between border-b" style={{ borderColor: 'var(--border)' }}>
|
||||
<h3 className="text-sm font-semibold truncate">{entity.name}</h3>
|
||||
<button onClick={onClose} className="p-1">
|
||||
<X size={14} style={{ color: 'var(--muted-foreground)' }} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div className="p-3 space-y-3 text-sm">
|
||||
<Section label="Type">{entity.type_ref.replace(/_go_cybersecurity$/, '').replace(/^osint_/, '')}</Section>
|
||||
<Section label="Status">{entity.status}</Section>
|
||||
{entity.description && <Section label="Description">{entity.description}</Section>}
|
||||
|
||||
{entity.metadata && Object.keys(entity.metadata).length > 0 && (
|
||||
<div>
|
||||
<label className="block text-xs font-semibold mb-1" style={{ color: 'var(--muted-foreground)' }}>Metadata</label>
|
||||
<div className="space-y-1">
|
||||
{Object.entries(entity.metadata).map(([k, v]) => (
|
||||
<div key={k} className="flex justify-between">
|
||||
<span style={{ color: 'var(--muted-foreground)' }}>{k}</span>
|
||||
<span className="font-mono text-xs">{String(v)}</span>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{entity.notes && (
|
||||
<div>
|
||||
<label className="block text-xs font-semibold mb-1" style={{ color: 'var(--muted-foreground)' }}>Notes</label>
|
||||
<p className="text-xs whitespace-pre-wrap" style={{ color: 'var(--foreground)' }}>{entity.notes}</p>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{entity.tags && entity.tags.length > 0 && (
|
||||
<div>
|
||||
<label className="block text-xs font-semibold mb-1" style={{ color: 'var(--muted-foreground)' }}>Tags</label>
|
||||
<div className="flex flex-wrap gap-1">
|
||||
{entity.tags.map(t => (
|
||||
<span key={t} className="px-1.5 py-0.5 rounded text-xs" style={{ background: 'var(--secondary)', color: 'var(--secondary-foreground)' }}>{t}</span>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{directRelations.length > 0 && (
|
||||
<div>
|
||||
<label className="block text-xs font-semibold mb-1" style={{ color: 'var(--muted-foreground)' }}>
|
||||
Relations ({directRelations.length})
|
||||
</label>
|
||||
<div className="space-y-1">
|
||||
{directRelations.map(r => {
|
||||
const isFrom = r.from_entity === entity.id
|
||||
return (
|
||||
<div key={r.id} className="flex items-center gap-1 text-xs">
|
||||
<ExternalLink size={10} style={{ color: 'var(--muted-foreground)' }} />
|
||||
<span>{isFrom ? '' : '<-'} {r.name} {isFrom ? '->' : ''}</span>
|
||||
<span className="font-medium">{isFrom ? r.to_entity : r.from_entity}</span>
|
||||
</div>
|
||||
)
|
||||
})}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
|
||||
function Section({ label, children }: { label: string; children: React.ReactNode }) {
|
||||
return (
|
||||
<div>
|
||||
<label className="block text-xs font-semibold" style={{ color: 'var(--muted-foreground)' }}>{label}</label>
|
||||
<span>{children}</span>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,160 @@
|
||||
import { useState, useEffect } from 'react'
|
||||
import type { Entity, EntityTypePreset, EntityInput } from '../types'
|
||||
import { SimpleSelect } from '@fn_library'
|
||||
import { X } from 'lucide-react'
|
||||
|
||||
interface Props {
|
||||
presets: EntityTypePreset[]
|
||||
entity: Entity | null // null = create, non-null = edit
|
||||
onSubmit: (input: EntityInput) => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export function EntityDialog({ presets, entity, onSubmit, onClose }: Props) {
|
||||
const [name, setName] = useState(entity?.name ?? '')
|
||||
const [typeRef, setTypeRef] = useState(entity?.type_ref ?? presets[0]?.type_ref ?? '')
|
||||
const [description, setDescription] = useState(entity?.description ?? '')
|
||||
const [notes, setNotes] = useState(entity?.notes ?? '')
|
||||
const [tagsStr, setTagsStr] = useState((entity?.tags ?? []).join(', '))
|
||||
const [metadata, setMetadata] = useState<Record<string, string>>(() => {
|
||||
const m: Record<string, string> = {}
|
||||
if (entity?.metadata) {
|
||||
for (const [k, v] of Object.entries(entity.metadata)) {
|
||||
m[k] = String(v ?? '')
|
||||
}
|
||||
}
|
||||
return m
|
||||
})
|
||||
|
||||
const currentPreset = presets.find(p => p.type_ref === typeRef)
|
||||
const metadataFields = currentPreset?.metadata_fields ?? []
|
||||
|
||||
// When type changes, reset metadata fields to match new type
|
||||
useEffect(() => {
|
||||
if (!entity) {
|
||||
const m: Record<string, string> = {}
|
||||
for (const f of metadataFields) {
|
||||
m[f] = metadata[f] ?? ''
|
||||
}
|
||||
setMetadata(m)
|
||||
}
|
||||
}, [typeRef]) // eslint-disable-line react-hooks/exhaustive-deps
|
||||
|
||||
const handleSubmit = () => {
|
||||
const cleanMeta: Record<string, unknown> = {}
|
||||
for (const [k, v] of Object.entries(metadata)) {
|
||||
if (v.trim()) {
|
||||
// Try parsing as number
|
||||
const num = Number(v)
|
||||
if (!isNaN(num) && v.trim() !== '') {
|
||||
cleanMeta[k] = num
|
||||
} else if (v === 'true') {
|
||||
cleanMeta[k] = true
|
||||
} else if (v === 'false') {
|
||||
cleanMeta[k] = false
|
||||
} else {
|
||||
cleanMeta[k] = v.trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onSubmit({
|
||||
name: name.trim(),
|
||||
type_ref: typeRef,
|
||||
description: description.trim(),
|
||||
tags: tagsStr.split(',').map(t => t.trim()).filter(Boolean),
|
||||
metadata: cleanMeta,
|
||||
notes: notes.trim(),
|
||||
})
|
||||
}
|
||||
|
||||
const inputStyle = {
|
||||
background: 'var(--input)',
|
||||
color: 'var(--foreground)',
|
||||
border: '1px solid var(--border)',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center" style={{ background: 'rgba(0,0,0,0.6)' }}>
|
||||
<div className="w-[520px] max-h-[85vh] overflow-y-auto rounded-lg p-5" style={{ background: 'var(--card)', border: '1px solid var(--border)' }}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-base font-semibold">{entity ? 'Edit Entity' : 'New Entity'}</h3>
|
||||
<button onClick={onClose} className="p-1"><X size={16} style={{ color: 'var(--muted-foreground)' }} /></button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-xs mb-1" style={{ color: 'var(--muted-foreground)' }}>Name</label>
|
||||
<input value={name} onChange={e => setName(e.target.value)} className="w-full px-3 py-1.5 rounded text-sm" style={inputStyle} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs mb-1" style={{ color: 'var(--muted-foreground)' }}>Type</label>
|
||||
<SimpleSelect
|
||||
value={typeRef}
|
||||
onValueChange={setTypeRef}
|
||||
options={presets.map(p => ({ value: p.type_ref, label: p.label }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs mb-1" style={{ color: 'var(--muted-foreground)' }}>Description</label>
|
||||
<input value={description} onChange={e => setDescription(e.target.value)} className="w-full px-3 py-1.5 rounded text-sm" style={inputStyle} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs mb-1" style={{ color: 'var(--muted-foreground)' }}>Tags (comma separated)</label>
|
||||
<input value={tagsStr} onChange={e => setTagsStr(e.target.value)} className="w-full px-3 py-1.5 rounded text-sm" style={inputStyle} placeholder="osint, high-risk" />
|
||||
</div>
|
||||
|
||||
{metadataFields.length > 0 && (
|
||||
<div>
|
||||
<label className="block text-xs mb-1 font-semibold" style={{ color: 'var(--muted-foreground)' }}>
|
||||
Metadata ({currentPreset?.label})
|
||||
</label>
|
||||
<div className="space-y-2">
|
||||
{metadataFields.map(field => (
|
||||
<div key={field} className="flex items-center gap-2">
|
||||
<span className="text-xs w-28 text-right" style={{ color: 'var(--muted-foreground)' }}>{field}</span>
|
||||
<input
|
||||
value={metadata[field] ?? ''}
|
||||
onChange={e => setMetadata(prev => ({ ...prev, [field]: e.target.value }))}
|
||||
className="flex-1 px-2 py-1 rounded text-sm"
|
||||
style={inputStyle}
|
||||
/>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div>
|
||||
<label className="block text-xs mb-1" style={{ color: 'var(--muted-foreground)' }}>Notes</label>
|
||||
<textarea
|
||||
value={notes}
|
||||
onChange={e => setNotes(e.target.value)}
|
||||
rows={3}
|
||||
className="w-full px-3 py-1.5 rounded text-sm resize-none"
|
||||
style={inputStyle}
|
||||
placeholder="Operational notes..."
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 mt-4">
|
||||
<button onClick={onClose} className="px-3 py-1.5 rounded text-sm" style={{ background: 'var(--secondary)', color: 'var(--secondary-foreground)' }}>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={!name.trim()}
|
||||
className="px-3 py-1.5 rounded text-sm font-medium disabled:opacity-40"
|
||||
style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}
|
||||
>
|
||||
{entity ? 'Update' : 'Create'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { useState } from 'react'
|
||||
import type { Entity, EntityTypePreset, EntityInput } from '../types'
|
||||
import { EntityDialog } from './EntityDialog'
|
||||
import { Plus, Pencil, Trash2 } from 'lucide-react'
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@fn_library'
|
||||
import { Badge } from '@fn_library'
|
||||
import { Button } from '@fn_library'
|
||||
import { AddEntity, UpdateEntity, DeleteEntity } from '../wailsjs/go/main/App'
|
||||
|
||||
interface Props {
|
||||
entities: Entity[]
|
||||
presets: EntityTypePreset[]
|
||||
onRefresh: () => void
|
||||
}
|
||||
|
||||
export function EntityTable({ entities, presets, onRefresh }: Props) {
|
||||
const [dialogOpen, setDialogOpen] = useState(false)
|
||||
const [editEntity, setEditEntity] = useState<Entity | null>(null)
|
||||
|
||||
const presetMap = Object.fromEntries(presets.map(p => [p.type_ref, p]))
|
||||
|
||||
const handleAdd = async (input: EntityInput) => {
|
||||
await AddEntity(input as never)
|
||||
setDialogOpen(false)
|
||||
onRefresh()
|
||||
}
|
||||
|
||||
const handleUpdate = async (input: EntityInput) => {
|
||||
if (editEntity) {
|
||||
await UpdateEntity(editEntity.id, input as never)
|
||||
setEditEntity(null)
|
||||
onRefresh()
|
||||
}
|
||||
}
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
await DeleteEntity(id)
|
||||
onRefresh()
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="mt-3">
|
||||
<CardHeader className="flex flex-row items-center justify-between py-3">
|
||||
<CardTitle className="text-base">Entities</CardTitle>
|
||||
<Button size="sm" onClick={() => setDialogOpen(true)}>
|
||||
<Plus size={14} className="mr-1" /> Add Entity
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b" style={{ borderColor: 'var(--border)' }}>
|
||||
<th className="text-left px-4 py-2 font-medium" style={{ color: 'var(--muted-foreground)' }}>Name</th>
|
||||
<th className="text-left px-4 py-2 font-medium" style={{ color: 'var(--muted-foreground)' }}>Type</th>
|
||||
<th className="text-left px-4 py-2 font-medium" style={{ color: 'var(--muted-foreground)' }}>Status</th>
|
||||
<th className="text-left px-4 py-2 font-medium" style={{ color: 'var(--muted-foreground)' }}>Notes</th>
|
||||
<th className="text-right px-4 py-2 font-medium" style={{ color: 'var(--muted-foreground)' }}>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{entities.map(e => {
|
||||
const preset = presetMap[e.type_ref]
|
||||
return (
|
||||
<tr key={e.id} className="border-b hover:opacity-90" style={{ borderColor: 'var(--border)' }}>
|
||||
<td className="px-4 py-2 font-medium">{e.name}</td>
|
||||
<td className="px-4 py-2">
|
||||
<Badge style={{ backgroundColor: preset?.color ?? 'var(--muted-foreground)', color: 'var(--primary-foreground)' }}>
|
||||
{preset?.label ?? e.type_ref}
|
||||
</Badge>
|
||||
</td>
|
||||
<td className="px-4 py-2">
|
||||
<span className="text-xs" style={{ color: e.status === 'active' ? 'var(--success)' : 'var(--muted-foreground)' }}>
|
||||
{e.status}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-4 py-2 max-w-48 truncate" style={{ color: 'var(--muted-foreground)' }}>
|
||||
{e.notes || '—'}
|
||||
</td>
|
||||
<td className="px-4 py-2 text-right">
|
||||
<button onClick={() => setEditEntity(e)} className="p-1 mr-1 rounded hover:opacity-80" style={{ color: 'var(--primary)' }}>
|
||||
<Pencil size={14} />
|
||||
</button>
|
||||
<button onClick={() => handleDelete(e.id)} className="p-1 rounded hover:opacity-80" style={{ color: 'var(--destructive)' }}>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
)
|
||||
})}
|
||||
{entities.length === 0 && (
|
||||
<tr><td colSpan={5} className="px-4 py-8 text-center" style={{ color: 'var(--muted-foreground)' }}>No entities yet</td></tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</CardContent>
|
||||
|
||||
{(dialogOpen || editEntity) && (
|
||||
<EntityDialog
|
||||
presets={presets}
|
||||
entity={editEntity}
|
||||
onSubmit={editEntity ? handleUpdate : handleAdd}
|
||||
onClose={() => { setDialogOpen(false); setEditEntity(null) }}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
import { GraphContainer } from '@graph'
|
||||
import type { GraphData as LibGraphData } from '@graph'
|
||||
import type { GraphData, EntityTypePreset } from '../types'
|
||||
|
||||
interface Props {
|
||||
data: GraphData
|
||||
presets: EntityTypePreset[]
|
||||
onNodeClick: (nodeId: string) => void
|
||||
onNodeDoubleClick: (nodeId: string) => void
|
||||
}
|
||||
|
||||
export function GraphView({ data, presets, onNodeClick, onNodeDoubleClick }: Props) {
|
||||
// Map our GraphData to the library's format (they're compatible but need the cast)
|
||||
const libData: LibGraphData = {
|
||||
nodes: data.nodes.map(n => ({
|
||||
id: n.id,
|
||||
label: n.label,
|
||||
type: n.type,
|
||||
color: n.color,
|
||||
size: n.size,
|
||||
x: n.x,
|
||||
y: n.y,
|
||||
})),
|
||||
edges: data.edges.map(e => ({
|
||||
id: e.id,
|
||||
source: e.source,
|
||||
target: e.target,
|
||||
label: e.label,
|
||||
color: e.color,
|
||||
size: e.size,
|
||||
type: e.type as 'arrow' | 'line',
|
||||
})),
|
||||
}
|
||||
|
||||
const nodeTypes = presets.map(p => ({
|
||||
type: p.type_ref,
|
||||
color: p.color,
|
||||
label: p.label,
|
||||
}))
|
||||
|
||||
if (data.nodes.length === 0) {
|
||||
return (
|
||||
<div className="flex items-center justify-center h-full">
|
||||
<p className="text-sm" style={{ color: 'var(--muted-foreground)' }}>
|
||||
No data to display. Add entities and relations to build the graph.
|
||||
</p>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
return (
|
||||
<GraphContainer
|
||||
data={libData}
|
||||
layout="organic"
|
||||
showToolbar
|
||||
showLegend
|
||||
showMinimap
|
||||
nodeTypes={nodeTypes}
|
||||
onNodeClick={n => onNodeClick(n.id)}
|
||||
onNodeDoubleClick={n => onNodeDoubleClick(n.id)}
|
||||
enableSelection
|
||||
selectionMode="multiple"
|
||||
theme={{ nodeSize: 8, edgeSize: 1 }}
|
||||
height="100%"
|
||||
/>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,103 @@
|
||||
import { useState } from 'react'
|
||||
import type { ProjectInfo } from '../types'
|
||||
import { Plus, Trash2, FolderOpen } from 'lucide-react'
|
||||
|
||||
interface Props {
|
||||
projects: ProjectInfo[]
|
||||
current: string
|
||||
onSwitch: (name: string) => void
|
||||
onCreate: (name: string) => void
|
||||
onDelete: (name: string) => void
|
||||
}
|
||||
|
||||
export function ProjectSidebar({ projects, current, onSwitch, onCreate, onDelete }: Props) {
|
||||
const [newName, setNewName] = useState('')
|
||||
const [showInput, setShowInput] = useState(false)
|
||||
|
||||
console.log('[ProjectSidebar] render: projects=', projects.length, 'current=', current, 'projects data:', JSON.stringify(projects))
|
||||
|
||||
const handleCreate = () => {
|
||||
const name = newName.trim()
|
||||
console.log('[ProjectSidebar] handleCreate: name=', JSON.stringify(name))
|
||||
if (name) {
|
||||
onCreate(name)
|
||||
setNewName('')
|
||||
setShowInput(false)
|
||||
} else {
|
||||
console.log('[ProjectSidebar] handleCreate: empty name, skipping')
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
<aside className="w-56 flex flex-col border-r" style={{ borderColor: 'var(--border)', background: 'var(--card)' }}>
|
||||
<div className="p-3 flex items-center justify-between border-b" style={{ borderColor: 'var(--border)' }}>
|
||||
<span className="text-xs font-bold uppercase tracking-wider" style={{ color: 'var(--muted-foreground)' }}>
|
||||
Projects
|
||||
</span>
|
||||
<button
|
||||
onClick={() => { console.log('[ProjectSidebar] toggling input'); setShowInput(!showInput) }}
|
||||
className="p-1 rounded hover:opacity-80"
|
||||
style={{ color: 'var(--primary)' }}
|
||||
>
|
||||
<Plus size={16} />
|
||||
</button>
|
||||
</div>
|
||||
|
||||
{showInput && (
|
||||
<div className="p-2 border-b" style={{ borderColor: 'var(--border)' }}>
|
||||
<input
|
||||
value={newName}
|
||||
onChange={e => setNewName(e.target.value)}
|
||||
onKeyDown={e => {
|
||||
console.log('[ProjectSidebar] keyDown:', e.key)
|
||||
if (e.key === 'Enter') handleCreate()
|
||||
}}
|
||||
placeholder="Project name..."
|
||||
autoFocus
|
||||
className="w-full px-2 py-1 rounded text-sm"
|
||||
style={{
|
||||
background: 'var(--input)',
|
||||
color: 'var(--foreground)',
|
||||
border: '1px solid var(--border)',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex-1 overflow-y-auto">
|
||||
{projects.map(p => (
|
||||
<button
|
||||
key={p.name}
|
||||
onClick={() => { console.log('[ProjectSidebar] switching to:', p.name); onSwitch(p.name) }}
|
||||
className="flex w-full items-center gap-2 px-3 py-2 cursor-pointer group text-left"
|
||||
style={{
|
||||
background: p.name === current ? 'var(--accent)' : 'transparent',
|
||||
color: p.name === current ? 'var(--accent-foreground)' : 'var(--foreground)',
|
||||
}}
|
||||
>
|
||||
<FolderOpen size={14} style={{ color: 'var(--muted-foreground)' }} />
|
||||
<div className="flex-1 min-w-0">
|
||||
<div className="text-sm truncate">{p.name}</div>
|
||||
<div className="text-xs" style={{ color: 'var(--muted-foreground)' }}>
|
||||
{p.entity_count}E / {p.relation_count}R
|
||||
</div>
|
||||
</div>
|
||||
<button
|
||||
onClick={e => { e.stopPropagation(); console.log('[ProjectSidebar] deleting:', p.name); onDelete(p.name) }}
|
||||
className="opacity-0 group-hover:opacity-100 p-1 rounded"
|
||||
style={{ color: 'var(--destructive)' }}
|
||||
>
|
||||
<Trash2 size={12} />
|
||||
</button>
|
||||
</button>
|
||||
))}
|
||||
|
||||
{projects.length === 0 && (
|
||||
<p className="p-3 text-xs" style={{ color: 'var(--muted-foreground)' }}>
|
||||
No projects yet
|
||||
</p>
|
||||
)}
|
||||
</div>
|
||||
</aside>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,107 @@
|
||||
import { useState } from 'react'
|
||||
import type { Entity, RelationInputDTO } from '../types'
|
||||
import { SimpleSelect } from '@fn_library'
|
||||
import { X } from 'lucide-react'
|
||||
|
||||
interface Props {
|
||||
entities: Entity[]
|
||||
relationPresets: string[]
|
||||
onSubmit: (input: RelationInputDTO) => void
|
||||
onClose: () => void
|
||||
}
|
||||
|
||||
export function RelationDialog({ entities, relationPresets, onSubmit, onClose }: Props) {
|
||||
const [name, setName] = useState(relationPresets[0] ?? '')
|
||||
const [fromEntity, setFromEntity] = useState(entities[0]?.id ?? '')
|
||||
const [toEntity, setToEntity] = useState(entities[1]?.id ?? entities[0]?.id ?? '')
|
||||
const [description, setDescription] = useState('')
|
||||
const [weight, setWeight] = useState('1.0')
|
||||
const [notes, setNotes] = useState('')
|
||||
|
||||
const handleSubmit = () => {
|
||||
const w = parseFloat(weight)
|
||||
onSubmit({
|
||||
name,
|
||||
from_entity: fromEntity,
|
||||
to_entity: toEntity,
|
||||
description,
|
||||
weight: isNaN(w) ? null : w,
|
||||
tags: [],
|
||||
notes,
|
||||
})
|
||||
}
|
||||
|
||||
const inputStyle = {
|
||||
background: 'var(--input)',
|
||||
color: 'var(--foreground)',
|
||||
border: '1px solid var(--border)',
|
||||
}
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center" style={{ background: 'rgba(0,0,0,0.6)' }}>
|
||||
<div className="w-[480px] rounded-lg p-5" style={{ background: 'var(--card)', border: '1px solid var(--border)' }}>
|
||||
<div className="flex items-center justify-between mb-4">
|
||||
<h3 className="text-base font-semibold">New Relation</h3>
|
||||
<button onClick={onClose} className="p-1"><X size={16} style={{ color: 'var(--muted-foreground)' }} /></button>
|
||||
</div>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div>
|
||||
<label className="block text-xs mb-1" style={{ color: 'var(--muted-foreground)' }}>Relation Type</label>
|
||||
<SimpleSelect
|
||||
value={name}
|
||||
onValueChange={setName}
|
||||
options={relationPresets.map(p => ({ value: p, label: p }))}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex gap-3">
|
||||
<div className="flex-1">
|
||||
<label className="block text-xs mb-1" style={{ color: 'var(--muted-foreground)' }}>From</label>
|
||||
<SimpleSelect
|
||||
value={fromEntity}
|
||||
onValueChange={setFromEntity}
|
||||
options={entities.map(e => ({ value: e.id, label: e.name }))}
|
||||
/>
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<label className="block text-xs mb-1" style={{ color: 'var(--muted-foreground)' }}>To</label>
|
||||
<SimpleSelect
|
||||
value={toEntity}
|
||||
onValueChange={setToEntity}
|
||||
options={entities.map(e => ({ value: e.id, label: e.name }))}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs mb-1" style={{ color: 'var(--muted-foreground)' }}>Description</label>
|
||||
<input value={description} onChange={e => setDescription(e.target.value)} className="w-full px-3 py-1.5 rounded text-sm" style={inputStyle} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs mb-1" style={{ color: 'var(--muted-foreground)' }}>Weight (0.0 - 1.0)</label>
|
||||
<input value={weight} onChange={e => setWeight(e.target.value)} type="number" step="0.1" min="0" max="1" className="w-full px-3 py-1.5 rounded text-sm" style={inputStyle} />
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label className="block text-xs mb-1" style={{ color: 'var(--muted-foreground)' }}>Notes</label>
|
||||
<textarea value={notes} onChange={e => setNotes(e.target.value)} rows={2} className="w-full px-3 py-1.5 rounded text-sm resize-none" style={inputStyle} />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-end gap-2 mt-4">
|
||||
<button onClick={onClose} className="px-3 py-1.5 rounded text-sm" style={{ background: 'var(--secondary)', color: 'var(--secondary-foreground)' }}>Cancel</button>
|
||||
<button
|
||||
onClick={handleSubmit}
|
||||
disabled={!name || fromEntity === toEntity}
|
||||
className="px-3 py-1.5 rounded text-sm font-medium disabled:opacity-40"
|
||||
style={{ background: 'var(--primary)', color: 'var(--primary-foreground)' }}
|
||||
>
|
||||
Create
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,82 @@
|
||||
import { useState } from 'react'
|
||||
import type { Relation, Entity, RelationInputDTO } from '../types'
|
||||
import { RelationDialog } from './RelationDialog'
|
||||
import { Plus, Trash2 } from 'lucide-react'
|
||||
import { Card, CardHeader, CardTitle, CardContent } from '@fn_library'
|
||||
import { Button } from '@fn_library'
|
||||
import { AddRelation, DeleteRelation } from '../wailsjs/go/main/App'
|
||||
|
||||
interface Props {
|
||||
relations: Relation[]
|
||||
entities: Entity[]
|
||||
relationPresets: string[]
|
||||
onRefresh: () => void
|
||||
}
|
||||
|
||||
export function RelationTable({ relations, entities, relationPresets, onRefresh }: Props) {
|
||||
const [dialogOpen, setDialogOpen] = useState(false)
|
||||
|
||||
const entityMap = Object.fromEntries(entities.map(e => [e.id, e.name]))
|
||||
|
||||
const handleAdd = async (input: RelationInputDTO) => {
|
||||
await AddRelation(input as never)
|
||||
setDialogOpen(false)
|
||||
onRefresh()
|
||||
}
|
||||
|
||||
const handleDelete = async (id: string) => {
|
||||
await DeleteRelation(id)
|
||||
onRefresh()
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="mt-3">
|
||||
<CardHeader className="flex flex-row items-center justify-between py-3">
|
||||
<CardTitle className="text-base">Relations</CardTitle>
|
||||
<Button size="sm" onClick={() => setDialogOpen(true)} disabled={entities.length < 2}>
|
||||
<Plus size={14} className="mr-1" /> Add Relation
|
||||
</Button>
|
||||
</CardHeader>
|
||||
<CardContent className="p-0">
|
||||
<table className="w-full text-sm">
|
||||
<thead>
|
||||
<tr className="border-b" style={{ borderColor: 'var(--border)' }}>
|
||||
<th className="text-left px-4 py-2 font-medium" style={{ color: 'var(--muted-foreground)' }}>From</th>
|
||||
<th className="text-left px-4 py-2 font-medium" style={{ color: 'var(--muted-foreground)' }}>Relation</th>
|
||||
<th className="text-left px-4 py-2 font-medium" style={{ color: 'var(--muted-foreground)' }}>To</th>
|
||||
<th className="text-left px-4 py-2 font-medium" style={{ color: 'var(--muted-foreground)' }}>Weight</th>
|
||||
<th className="text-right px-4 py-2 font-medium" style={{ color: 'var(--muted-foreground)' }}>Actions</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{relations.map(r => (
|
||||
<tr key={r.id} className="border-b" style={{ borderColor: 'var(--border)' }}>
|
||||
<td className="px-4 py-2">{entityMap[r.from_entity] ?? r.from_entity}</td>
|
||||
<td className="px-4 py-2 font-medium">{r.name}</td>
|
||||
<td className="px-4 py-2">{entityMap[r.to_entity] ?? r.to_entity}</td>
|
||||
<td className="px-4 py-2" style={{ color: 'var(--muted-foreground)' }}>{r.weight?.toFixed(2) ?? '—'}</td>
|
||||
<td className="px-4 py-2 text-right">
|
||||
<button onClick={() => handleDelete(r.id)} className="p-1 rounded hover:opacity-80" style={{ color: 'var(--destructive)' }}>
|
||||
<Trash2 size={14} />
|
||||
</button>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
{relations.length === 0 && (
|
||||
<tr><td colSpan={5} className="px-4 py-8 text-center" style={{ color: 'var(--muted-foreground)' }}>No relations yet</td></tr>
|
||||
)}
|
||||
</tbody>
|
||||
</table>
|
||||
</CardContent>
|
||||
|
||||
{dialogOpen && (
|
||||
<RelationDialog
|
||||
entities={entities}
|
||||
relationPresets={relationPresets}
|
||||
onSubmit={handleAdd}
|
||||
onClose={() => setDialogOpen(false)}
|
||||
/>
|
||||
)}
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import { useState, useRef, useEffect } from 'react'
|
||||
import { Search, X } from 'lucide-react'
|
||||
|
||||
interface Props {
|
||||
onSearch: (query: string) => void
|
||||
}
|
||||
|
||||
export function SearchBar({ onSearch }: Props) {
|
||||
const [query, setQuery] = useState('')
|
||||
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
|
||||
useEffect(() => {
|
||||
if (timerRef.current) clearTimeout(timerRef.current)
|
||||
timerRef.current = setTimeout(() => {
|
||||
onSearch(query)
|
||||
}, 300)
|
||||
return () => { if (timerRef.current) clearTimeout(timerRef.current) }
|
||||
}, [query, onSearch])
|
||||
|
||||
return (
|
||||
<div className="flex-1 flex items-center gap-2 px-2 py-1 rounded" style={{ background: 'var(--input)', border: '1px solid var(--border)' }}>
|
||||
<Search size={14} style={{ color: 'var(--muted-foreground)' }} />
|
||||
<input
|
||||
value={query}
|
||||
onChange={e => setQuery(e.target.value)}
|
||||
placeholder="Search entities..."
|
||||
className="flex-1 bg-transparent text-sm outline-none"
|
||||
style={{ color: 'var(--foreground)' }}
|
||||
/>
|
||||
{query && (
|
||||
<button onClick={() => setQuery('')} className="p-0.5">
|
||||
<X size={12} style={{ color: 'var(--muted-foreground)' }} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
Vendored
+61
@@ -0,0 +1,61 @@
|
||||
declare module '@fn_library' {
|
||||
export const Tabs: React.FC<{ value: string; onValueChange: (v: string) => void; className?: string; children: React.ReactNode }>
|
||||
export const TabsList: React.FC<{ className?: string; children: React.ReactNode }>
|
||||
export const TabsTrigger: React.FC<{ value: string; children: React.ReactNode }>
|
||||
export const TabsContent: React.FC<{ value: string; className?: string; children: React.ReactNode }>
|
||||
export const Card: React.FC<{ className?: string; children: React.ReactNode }>
|
||||
export const CardHeader: React.FC<{ className?: string; children: React.ReactNode }>
|
||||
export const CardTitle: React.FC<{ className?: string; children: React.ReactNode }>
|
||||
export const CardContent: React.FC<{ className?: string; children: React.ReactNode }>
|
||||
export const Badge: React.FC<{ className?: string; style?: React.CSSProperties; children: React.ReactNode }>
|
||||
export const Button: React.FC<{ size?: string; variant?: string; onClick?: () => void; disabled?: boolean; className?: string; children: React.ReactNode }>
|
||||
export interface SimpleSelectOption { value: string; label: string; disabled?: boolean }
|
||||
export function SimpleSelect(props: { value: string; onValueChange: (value: string) => void; options: SimpleSelectOption[]; placeholder?: string; disabled?: boolean; size?: 'sm' | 'default'; className?: string }): React.ReactElement
|
||||
export function SearchBar(props: { onSearch: (query: string) => void; placeholder?: string; debounceMs?: number; className?: string }): React.ReactElement
|
||||
}
|
||||
|
||||
declare module '@graph' {
|
||||
export interface GraphNode {
|
||||
id: string
|
||||
label?: string
|
||||
type?: string
|
||||
color?: string
|
||||
size?: number
|
||||
x?: number
|
||||
y?: number
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export interface GraphEdge {
|
||||
id: string
|
||||
source: string
|
||||
target: string
|
||||
label?: string
|
||||
color?: string
|
||||
size?: number
|
||||
type?: 'arrow' | 'line'
|
||||
[key: string]: unknown
|
||||
}
|
||||
|
||||
export interface GraphData {
|
||||
nodes: GraphNode[]
|
||||
edges: GraphEdge[]
|
||||
}
|
||||
|
||||
export const GraphContainer: React.FC<{
|
||||
data: GraphData
|
||||
layout?: string
|
||||
showToolbar?: boolean
|
||||
showLegend?: boolean
|
||||
showMinimap?: boolean
|
||||
nodeTypes?: Array<{ type: string; color: string; label: string }>
|
||||
onNodeClick?: (node: GraphNode) => void
|
||||
onNodeDoubleClick?: (node: GraphNode) => void
|
||||
enableSelection?: boolean
|
||||
selectionMode?: string
|
||||
theme?: Record<string, unknown>
|
||||
height?: string | number
|
||||
}>
|
||||
}
|
||||
|
||||
declare module '*.css' {}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { clsx, type ClassValue } from "clsx"
|
||||
import { twMerge } from "tailwind-merge"
|
||||
|
||||
export function cn(...inputs: ClassValue[]) {
|
||||
return twMerge(clsx(inputs))
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
import { StrictMode } from 'react'
|
||||
import { createRoot } from 'react-dom/client'
|
||||
import './app.css'
|
||||
import App from './App'
|
||||
|
||||
createRoot(document.getElementById('root')!).render(
|
||||
<StrictMode>
|
||||
<App />
|
||||
</StrictMode>,
|
||||
)
|
||||
@@ -0,0 +1,121 @@
|
||||
export interface ProjectInfo {
|
||||
name: string
|
||||
description: string
|
||||
entity_count: number
|
||||
relation_count: number
|
||||
}
|
||||
|
||||
export interface Entity {
|
||||
id: string
|
||||
name: string
|
||||
type_ref: string
|
||||
status: string
|
||||
description: string
|
||||
domain: string
|
||||
tags: string[]
|
||||
source: string
|
||||
metadata: Record<string, unknown>
|
||||
notes: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface Relation {
|
||||
id: string
|
||||
name: string
|
||||
from_entity: string
|
||||
to_entity: string
|
||||
via: string
|
||||
description: string
|
||||
purity: string
|
||||
direction: string
|
||||
weight: number | null
|
||||
status: string
|
||||
tags: string[]
|
||||
notes: string
|
||||
created_at: string
|
||||
updated_at: string
|
||||
}
|
||||
|
||||
export interface EntityTypePreset {
|
||||
type_ref: string
|
||||
label: string
|
||||
color: string
|
||||
metadata_fields: string[]
|
||||
}
|
||||
|
||||
export interface GraphData {
|
||||
nodes: GraphNode[]
|
||||
edges: GraphEdge[]
|
||||
}
|
||||
|
||||
export interface GraphNode {
|
||||
id: string
|
||||
label: string
|
||||
type: string
|
||||
color: string
|
||||
size: number
|
||||
x: number
|
||||
y: number
|
||||
extra?: Record<string, unknown>
|
||||
}
|
||||
|
||||
export interface GraphEdge {
|
||||
id: string
|
||||
source: string
|
||||
target: string
|
||||
label: string
|
||||
color: string
|
||||
size: number
|
||||
type: string
|
||||
}
|
||||
|
||||
export interface EntityInput {
|
||||
name: string
|
||||
type_ref: string
|
||||
description: string
|
||||
tags: string[]
|
||||
metadata: Record<string, unknown>
|
||||
notes: string
|
||||
}
|
||||
|
||||
export interface RelationInputDTO {
|
||||
name: string
|
||||
from_entity: string
|
||||
to_entity: string
|
||||
description: string
|
||||
weight: number | null
|
||||
tags: string[]
|
||||
notes: string
|
||||
}
|
||||
|
||||
export interface Assertion {
|
||||
id: string
|
||||
entity_id: string
|
||||
name: string
|
||||
kind: string
|
||||
rule: string
|
||||
severity: string
|
||||
description: string
|
||||
active: boolean
|
||||
created_at: string
|
||||
}
|
||||
|
||||
export interface AssertionResult {
|
||||
id: string
|
||||
assertion_id: string
|
||||
execution_id: string
|
||||
status: string
|
||||
value: Record<string, unknown>
|
||||
message: string
|
||||
evaluated_at: string
|
||||
}
|
||||
|
||||
export interface AssertionInput {
|
||||
entity_id: string
|
||||
name: string
|
||||
kind: string
|
||||
rule: string
|
||||
severity: string
|
||||
description: string
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
import {main} from '../models';
|
||||
import {fn_operations} from '../models';
|
||||
|
||||
export function AddAssertion(arg1:main.AssertionInput):Promise<string>;
|
||||
|
||||
export function AddEntity(arg1:main.EntityInput):Promise<string>;
|
||||
|
||||
export function AddRelation(arg1:main.RelationInputDTO):Promise<string>;
|
||||
|
||||
export function CreateProject(arg1:string):Promise<main.ProjectInfo>;
|
||||
|
||||
export function DeleteAssertion(arg1:string):Promise<void>;
|
||||
|
||||
export function DeleteEntity(arg1:string):Promise<void>;
|
||||
|
||||
export function DeleteProject(arg1:string):Promise<void>;
|
||||
|
||||
export function DeleteRelation(arg1:string):Promise<void>;
|
||||
|
||||
export function EvalAssertions(arg1:string):Promise<Array<fn_operations.AssertionResult>>;
|
||||
|
||||
export function GetCurrentProject():Promise<string>;
|
||||
|
||||
export function GetEntity(arg1:string):Promise<fn_operations.Entity>;
|
||||
|
||||
export function GetEntityNeighbors(arg1:string,arg2:number):Promise<main.GraphData>;
|
||||
|
||||
export function GetEntityPresets():Promise<Array<main.EntityTypePreset>>;
|
||||
|
||||
export function GetFilteredGraph(arg1:Array<string>):Promise<main.GraphData>;
|
||||
|
||||
export function GetGraphData():Promise<main.GraphData>;
|
||||
|
||||
export function GetRelationPresets():Promise<Array<string>>;
|
||||
|
||||
export function ListAssertions(arg1:string):Promise<Array<fn_operations.Assertion>>;
|
||||
|
||||
export function ListEntities():Promise<Array<fn_operations.Entity>>;
|
||||
|
||||
export function ListProjects():Promise<Array<main.ProjectInfo>>;
|
||||
|
||||
export function ListRelations():Promise<Array<fn_operations.Relation>>;
|
||||
|
||||
export function SearchEntities(arg1:string):Promise<Array<fn_operations.Entity>>;
|
||||
|
||||
export function SearchGraph(arg1:string):Promise<main.GraphData>;
|
||||
|
||||
export function SwitchProject(arg1:string):Promise<void>;
|
||||
|
||||
export function UpdateEntity(arg1:string,arg2:main.EntityInput):Promise<void>;
|
||||
|
||||
export function UpdateRelation(arg1:string,arg2:main.RelationInputDTO):Promise<void>;
|
||||
Executable
+103
@@ -0,0 +1,103 @@
|
||||
// @ts-check
|
||||
// Cynhyrchwyd y ffeil hon yn awtomatig. PEIDIWCH Â MODIWL
|
||||
// This file is automatically generated. DO NOT EDIT
|
||||
|
||||
export function AddAssertion(arg1) {
|
||||
return window['go']['main']['App']['AddAssertion'](arg1);
|
||||
}
|
||||
|
||||
export function AddEntity(arg1) {
|
||||
return window['go']['main']['App']['AddEntity'](arg1);
|
||||
}
|
||||
|
||||
export function AddRelation(arg1) {
|
||||
return window['go']['main']['App']['AddRelation'](arg1);
|
||||
}
|
||||
|
||||
export function CreateProject(arg1) {
|
||||
return window['go']['main']['App']['CreateProject'](arg1);
|
||||
}
|
||||
|
||||
export function DeleteAssertion(arg1) {
|
||||
return window['go']['main']['App']['DeleteAssertion'](arg1);
|
||||
}
|
||||
|
||||
export function DeleteEntity(arg1) {
|
||||
return window['go']['main']['App']['DeleteEntity'](arg1);
|
||||
}
|
||||
|
||||
export function DeleteProject(arg1) {
|
||||
return window['go']['main']['App']['DeleteProject'](arg1);
|
||||
}
|
||||
|
||||
export function DeleteRelation(arg1) {
|
||||
return window['go']['main']['App']['DeleteRelation'](arg1);
|
||||
}
|
||||
|
||||
export function EvalAssertions(arg1) {
|
||||
return window['go']['main']['App']['EvalAssertions'](arg1);
|
||||
}
|
||||
|
||||
export function GetCurrentProject() {
|
||||
return window['go']['main']['App']['GetCurrentProject']();
|
||||
}
|
||||
|
||||
export function GetEntity(arg1) {
|
||||
return window['go']['main']['App']['GetEntity'](arg1);
|
||||
}
|
||||
|
||||
export function GetEntityNeighbors(arg1, arg2) {
|
||||
return window['go']['main']['App']['GetEntityNeighbors'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function GetEntityPresets() {
|
||||
return window['go']['main']['App']['GetEntityPresets']();
|
||||
}
|
||||
|
||||
export function GetFilteredGraph(arg1) {
|
||||
return window['go']['main']['App']['GetFilteredGraph'](arg1);
|
||||
}
|
||||
|
||||
export function GetGraphData() {
|
||||
return window['go']['main']['App']['GetGraphData']();
|
||||
}
|
||||
|
||||
export function GetRelationPresets() {
|
||||
return window['go']['main']['App']['GetRelationPresets']();
|
||||
}
|
||||
|
||||
export function ListAssertions(arg1) {
|
||||
return window['go']['main']['App']['ListAssertions'](arg1);
|
||||
}
|
||||
|
||||
export function ListEntities() {
|
||||
return window['go']['main']['App']['ListEntities']();
|
||||
}
|
||||
|
||||
export function ListProjects() {
|
||||
return window['go']['main']['App']['ListProjects']();
|
||||
}
|
||||
|
||||
export function ListRelations() {
|
||||
return window['go']['main']['App']['ListRelations']();
|
||||
}
|
||||
|
||||
export function SearchEntities(arg1) {
|
||||
return window['go']['main']['App']['SearchEntities'](arg1);
|
||||
}
|
||||
|
||||
export function SearchGraph(arg1) {
|
||||
return window['go']['main']['App']['SearchGraph'](arg1);
|
||||
}
|
||||
|
||||
export function SwitchProject(arg1) {
|
||||
return window['go']['main']['App']['SwitchProject'](arg1);
|
||||
}
|
||||
|
||||
export function UpdateEntity(arg1, arg2) {
|
||||
return window['go']['main']['App']['UpdateEntity'](arg1, arg2);
|
||||
}
|
||||
|
||||
export function UpdateRelation(arg1, arg2) {
|
||||
return window['go']['main']['App']['UpdateRelation'](arg1, arg2);
|
||||
}
|
||||
Executable
+408
@@ -0,0 +1,408 @@
|
||||
export namespace fn_operations {
|
||||
|
||||
export class Assertion {
|
||||
id: string;
|
||||
entity_id: string;
|
||||
name: string;
|
||||
kind: string;
|
||||
rule: string;
|
||||
severity: string;
|
||||
description: string;
|
||||
active: boolean;
|
||||
// Go type: time
|
||||
created_at: any;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new Assertion(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.id = source["id"];
|
||||
this.entity_id = source["entity_id"];
|
||||
this.name = source["name"];
|
||||
this.kind = source["kind"];
|
||||
this.rule = source["rule"];
|
||||
this.severity = source["severity"];
|
||||
this.description = source["description"];
|
||||
this.active = source["active"];
|
||||
this.created_at = this.convertValues(source["created_at"], null);
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
if (!a) {
|
||||
return a;
|
||||
}
|
||||
if (a.slice && a.map) {
|
||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||
} else if ("object" === typeof a) {
|
||||
if (asMap) {
|
||||
for (const key of Object.keys(a)) {
|
||||
a[key] = new classs(a[key]);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return new classs(a);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
export class AssertionResult {
|
||||
id: string;
|
||||
assertion_id: string;
|
||||
execution_id: string;
|
||||
status: string;
|
||||
value: Record<string, any>;
|
||||
message: string;
|
||||
// Go type: time
|
||||
evaluated_at: any;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new AssertionResult(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.id = source["id"];
|
||||
this.assertion_id = source["assertion_id"];
|
||||
this.execution_id = source["execution_id"];
|
||||
this.status = source["status"];
|
||||
this.value = source["value"];
|
||||
this.message = source["message"];
|
||||
this.evaluated_at = this.convertValues(source["evaluated_at"], null);
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
if (!a) {
|
||||
return a;
|
||||
}
|
||||
if (a.slice && a.map) {
|
||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||
} else if ("object" === typeof a) {
|
||||
if (asMap) {
|
||||
for (const key of Object.keys(a)) {
|
||||
a[key] = new classs(a[key]);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return new classs(a);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
export class Entity {
|
||||
id: string;
|
||||
name: string;
|
||||
type_ref: string;
|
||||
status: string;
|
||||
description: string;
|
||||
domain: string;
|
||||
tags: string[];
|
||||
source: string;
|
||||
metadata: Record<string, any>;
|
||||
notes: string;
|
||||
// Go type: time
|
||||
created_at: any;
|
||||
// Go type: time
|
||||
updated_at: any;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new Entity(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.id = source["id"];
|
||||
this.name = source["name"];
|
||||
this.type_ref = source["type_ref"];
|
||||
this.status = source["status"];
|
||||
this.description = source["description"];
|
||||
this.domain = source["domain"];
|
||||
this.tags = source["tags"];
|
||||
this.source = source["source"];
|
||||
this.metadata = source["metadata"];
|
||||
this.notes = source["notes"];
|
||||
this.created_at = this.convertValues(source["created_at"], null);
|
||||
this.updated_at = this.convertValues(source["updated_at"], null);
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
if (!a) {
|
||||
return a;
|
||||
}
|
||||
if (a.slice && a.map) {
|
||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||
} else if ("object" === typeof a) {
|
||||
if (asMap) {
|
||||
for (const key of Object.keys(a)) {
|
||||
a[key] = new classs(a[key]);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return new classs(a);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
export class Relation {
|
||||
id: string;
|
||||
name: string;
|
||||
from_entity: string;
|
||||
to_entity: string;
|
||||
via: string;
|
||||
description: string;
|
||||
purity: string;
|
||||
direction: string;
|
||||
weight?: number;
|
||||
status: string;
|
||||
// Go type: time
|
||||
started_at?: any;
|
||||
// Go type: time
|
||||
ended_at?: any;
|
||||
order?: number;
|
||||
tags: string[];
|
||||
notes: string;
|
||||
// Go type: time
|
||||
created_at: any;
|
||||
// Go type: time
|
||||
updated_at: any;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new Relation(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.id = source["id"];
|
||||
this.name = source["name"];
|
||||
this.from_entity = source["from_entity"];
|
||||
this.to_entity = source["to_entity"];
|
||||
this.via = source["via"];
|
||||
this.description = source["description"];
|
||||
this.purity = source["purity"];
|
||||
this.direction = source["direction"];
|
||||
this.weight = source["weight"];
|
||||
this.status = source["status"];
|
||||
this.started_at = this.convertValues(source["started_at"], null);
|
||||
this.ended_at = this.convertValues(source["ended_at"], null);
|
||||
this.order = source["order"];
|
||||
this.tags = source["tags"];
|
||||
this.notes = source["notes"];
|
||||
this.created_at = this.convertValues(source["created_at"], null);
|
||||
this.updated_at = this.convertValues(source["updated_at"], null);
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
if (!a) {
|
||||
return a;
|
||||
}
|
||||
if (a.slice && a.map) {
|
||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||
} else if ("object" === typeof a) {
|
||||
if (asMap) {
|
||||
for (const key of Object.keys(a)) {
|
||||
a[key] = new classs(a[key]);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return new classs(a);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
export namespace main {
|
||||
|
||||
export class AssertionInput {
|
||||
entity_id: string;
|
||||
name: string;
|
||||
kind: string;
|
||||
rule: string;
|
||||
severity: string;
|
||||
description: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new AssertionInput(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.entity_id = source["entity_id"];
|
||||
this.name = source["name"];
|
||||
this.kind = source["kind"];
|
||||
this.rule = source["rule"];
|
||||
this.severity = source["severity"];
|
||||
this.description = source["description"];
|
||||
}
|
||||
}
|
||||
export class EntityInput {
|
||||
name: string;
|
||||
type_ref: string;
|
||||
description: string;
|
||||
tags: string[];
|
||||
metadata: Record<string, any>;
|
||||
notes: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new EntityInput(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.name = source["name"];
|
||||
this.type_ref = source["type_ref"];
|
||||
this.description = source["description"];
|
||||
this.tags = source["tags"];
|
||||
this.metadata = source["metadata"];
|
||||
this.notes = source["notes"];
|
||||
}
|
||||
}
|
||||
export class EntityTypePreset {
|
||||
type_ref: string;
|
||||
label: string;
|
||||
color: string;
|
||||
metadata_fields: string[];
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new EntityTypePreset(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.type_ref = source["type_ref"];
|
||||
this.label = source["label"];
|
||||
this.color = source["color"];
|
||||
this.metadata_fields = source["metadata_fields"];
|
||||
}
|
||||
}
|
||||
export class GraphEdge {
|
||||
id: string;
|
||||
source: string;
|
||||
target: string;
|
||||
label: string;
|
||||
color: string;
|
||||
size: number;
|
||||
type: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new GraphEdge(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.id = source["id"];
|
||||
this.source = source["source"];
|
||||
this.target = source["target"];
|
||||
this.label = source["label"];
|
||||
this.color = source["color"];
|
||||
this.size = source["size"];
|
||||
this.type = source["type"];
|
||||
}
|
||||
}
|
||||
export class GraphNode {
|
||||
id: string;
|
||||
label: string;
|
||||
type: string;
|
||||
color: string;
|
||||
size: number;
|
||||
x: number;
|
||||
y: number;
|
||||
extra?: Record<string, any>;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new GraphNode(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.id = source["id"];
|
||||
this.label = source["label"];
|
||||
this.type = source["type"];
|
||||
this.color = source["color"];
|
||||
this.size = source["size"];
|
||||
this.x = source["x"];
|
||||
this.y = source["y"];
|
||||
this.extra = source["extra"];
|
||||
}
|
||||
}
|
||||
export class GraphData {
|
||||
nodes: GraphNode[];
|
||||
edges: GraphEdge[];
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new GraphData(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.nodes = this.convertValues(source["nodes"], GraphNode);
|
||||
this.edges = this.convertValues(source["edges"], GraphEdge);
|
||||
}
|
||||
|
||||
convertValues(a: any, classs: any, asMap: boolean = false): any {
|
||||
if (!a) {
|
||||
return a;
|
||||
}
|
||||
if (a.slice && a.map) {
|
||||
return (a as any[]).map(elem => this.convertValues(elem, classs));
|
||||
} else if ("object" === typeof a) {
|
||||
if (asMap) {
|
||||
for (const key of Object.keys(a)) {
|
||||
a[key] = new classs(a[key]);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
return new classs(a);
|
||||
}
|
||||
return a;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
export class ProjectInfo {
|
||||
name: string;
|
||||
description: string;
|
||||
entity_count: number;
|
||||
relation_count: number;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new ProjectInfo(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.name = source["name"];
|
||||
this.description = source["description"];
|
||||
this.entity_count = source["entity_count"];
|
||||
this.relation_count = source["relation_count"];
|
||||
}
|
||||
}
|
||||
export class RelationInputDTO {
|
||||
name: string;
|
||||
from_entity: string;
|
||||
to_entity: string;
|
||||
description: string;
|
||||
weight?: number;
|
||||
tags: string[];
|
||||
notes: string;
|
||||
|
||||
static createFrom(source: any = {}) {
|
||||
return new RelationInputDTO(source);
|
||||
}
|
||||
|
||||
constructor(source: any = {}) {
|
||||
if ('string' === typeof source) source = JSON.parse(source);
|
||||
this.name = source["name"];
|
||||
this.from_entity = source["from_entity"];
|
||||
this.to_entity = source["to_entity"];
|
||||
this.description = source["description"];
|
||||
this.weight = source["weight"];
|
||||
this.tags = source["tags"];
|
||||
this.notes = source["notes"];
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
{
|
||||
"name": "@wailsapp/runtime",
|
||||
"version": "2.0.0",
|
||||
"description": "Wails Javascript runtime library",
|
||||
"main": "runtime.js",
|
||||
"types": "runtime.d.ts",
|
||||
"scripts": {
|
||||
},
|
||||
"repository": {
|
||||
"type": "git",
|
||||
"url": "git+https://github.com/wailsapp/wails.git"
|
||||
},
|
||||
"keywords": [
|
||||
"Wails",
|
||||
"Javascript",
|
||||
"Go"
|
||||
],
|
||||
"author": "Lea Anthony <lea.anthony@gmail.com>",
|
||||
"license": "MIT",
|
||||
"bugs": {
|
||||
"url": "https://github.com/wailsapp/wails/issues"
|
||||
},
|
||||
"homepage": "https://github.com/wailsapp/wails#readme"
|
||||
}
|
||||
+249
@@ -0,0 +1,249 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The electron alternative for Go
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
|
||||
export interface Position {
|
||||
x: number;
|
||||
y: number;
|
||||
}
|
||||
|
||||
export interface Size {
|
||||
w: number;
|
||||
h: number;
|
||||
}
|
||||
|
||||
export interface Screen {
|
||||
isCurrent: boolean;
|
||||
isPrimary: boolean;
|
||||
width : number
|
||||
height : number
|
||||
}
|
||||
|
||||
// Environment information such as platform, buildtype, ...
|
||||
export interface EnvironmentInfo {
|
||||
buildType: string;
|
||||
platform: string;
|
||||
arch: string;
|
||||
}
|
||||
|
||||
// [EventsEmit](https://wails.io/docs/reference/runtime/events#eventsemit)
|
||||
// emits the given event. Optional data may be passed with the event.
|
||||
// This will trigger any event listeners.
|
||||
export function EventsEmit(eventName: string, ...data: any): void;
|
||||
|
||||
// [EventsOn](https://wails.io/docs/reference/runtime/events#eventson) sets up a listener for the given event name.
|
||||
export function EventsOn(eventName: string, callback: (...data: any) => void): () => void;
|
||||
|
||||
// [EventsOnMultiple](https://wails.io/docs/reference/runtime/events#eventsonmultiple)
|
||||
// sets up a listener for the given event name, but will only trigger a given number times.
|
||||
export function EventsOnMultiple(eventName: string, callback: (...data: any) => void, maxCallbacks: number): () => void;
|
||||
|
||||
// [EventsOnce](https://wails.io/docs/reference/runtime/events#eventsonce)
|
||||
// sets up a listener for the given event name, but will only trigger once.
|
||||
export function EventsOnce(eventName: string, callback: (...data: any) => void): () => void;
|
||||
|
||||
// [EventsOff](https://wails.io/docs/reference/runtime/events#eventsoff)
|
||||
// unregisters the listener for the given event name.
|
||||
export function EventsOff(eventName: string, ...additionalEventNames: string[]): void;
|
||||
|
||||
// [EventsOffAll](https://wails.io/docs/reference/runtime/events#eventsoffall)
|
||||
// unregisters all listeners.
|
||||
export function EventsOffAll(): void;
|
||||
|
||||
// [LogPrint](https://wails.io/docs/reference/runtime/log#logprint)
|
||||
// logs the given message as a raw message
|
||||
export function LogPrint(message: string): void;
|
||||
|
||||
// [LogTrace](https://wails.io/docs/reference/runtime/log#logtrace)
|
||||
// logs the given message at the `trace` log level.
|
||||
export function LogTrace(message: string): void;
|
||||
|
||||
// [LogDebug](https://wails.io/docs/reference/runtime/log#logdebug)
|
||||
// logs the given message at the `debug` log level.
|
||||
export function LogDebug(message: string): void;
|
||||
|
||||
// [LogError](https://wails.io/docs/reference/runtime/log#logerror)
|
||||
// logs the given message at the `error` log level.
|
||||
export function LogError(message: string): void;
|
||||
|
||||
// [LogFatal](https://wails.io/docs/reference/runtime/log#logfatal)
|
||||
// logs the given message at the `fatal` log level.
|
||||
// The application will quit after calling this method.
|
||||
export function LogFatal(message: string): void;
|
||||
|
||||
// [LogInfo](https://wails.io/docs/reference/runtime/log#loginfo)
|
||||
// logs the given message at the `info` log level.
|
||||
export function LogInfo(message: string): void;
|
||||
|
||||
// [LogWarning](https://wails.io/docs/reference/runtime/log#logwarning)
|
||||
// logs the given message at the `warning` log level.
|
||||
export function LogWarning(message: string): void;
|
||||
|
||||
// [WindowReload](https://wails.io/docs/reference/runtime/window#windowreload)
|
||||
// Forces a reload by the main application as well as connected browsers.
|
||||
export function WindowReload(): void;
|
||||
|
||||
// [WindowReloadApp](https://wails.io/docs/reference/runtime/window#windowreloadapp)
|
||||
// Reloads the application frontend.
|
||||
export function WindowReloadApp(): void;
|
||||
|
||||
// [WindowSetAlwaysOnTop](https://wails.io/docs/reference/runtime/window#windowsetalwaysontop)
|
||||
// Sets the window AlwaysOnTop or not on top.
|
||||
export function WindowSetAlwaysOnTop(b: boolean): void;
|
||||
|
||||
// [WindowSetSystemDefaultTheme](https://wails.io/docs/next/reference/runtime/window#windowsetsystemdefaulttheme)
|
||||
// *Windows only*
|
||||
// Sets window theme to system default (dark/light).
|
||||
export function WindowSetSystemDefaultTheme(): void;
|
||||
|
||||
// [WindowSetLightTheme](https://wails.io/docs/next/reference/runtime/window#windowsetlighttheme)
|
||||
// *Windows only*
|
||||
// Sets window to light theme.
|
||||
export function WindowSetLightTheme(): void;
|
||||
|
||||
// [WindowSetDarkTheme](https://wails.io/docs/next/reference/runtime/window#windowsetdarktheme)
|
||||
// *Windows only*
|
||||
// Sets window to dark theme.
|
||||
export function WindowSetDarkTheme(): void;
|
||||
|
||||
// [WindowCenter](https://wails.io/docs/reference/runtime/window#windowcenter)
|
||||
// Centers the window on the monitor the window is currently on.
|
||||
export function WindowCenter(): void;
|
||||
|
||||
// [WindowSetTitle](https://wails.io/docs/reference/runtime/window#windowsettitle)
|
||||
// Sets the text in the window title bar.
|
||||
export function WindowSetTitle(title: string): void;
|
||||
|
||||
// [WindowFullscreen](https://wails.io/docs/reference/runtime/window#windowfullscreen)
|
||||
// Makes the window full screen.
|
||||
export function WindowFullscreen(): void;
|
||||
|
||||
// [WindowUnfullscreen](https://wails.io/docs/reference/runtime/window#windowunfullscreen)
|
||||
// Restores the previous window dimensions and position prior to full screen.
|
||||
export function WindowUnfullscreen(): void;
|
||||
|
||||
// [WindowIsFullscreen](https://wails.io/docs/reference/runtime/window#windowisfullscreen)
|
||||
// Returns the state of the window, i.e. whether the window is in full screen mode or not.
|
||||
export function WindowIsFullscreen(): Promise<boolean>;
|
||||
|
||||
// [WindowSetSize](https://wails.io/docs/reference/runtime/window#windowsetsize)
|
||||
// Sets the width and height of the window.
|
||||
export function WindowSetSize(width: number, height: number): void;
|
||||
|
||||
// [WindowGetSize](https://wails.io/docs/reference/runtime/window#windowgetsize)
|
||||
// Gets the width and height of the window.
|
||||
export function WindowGetSize(): Promise<Size>;
|
||||
|
||||
// [WindowSetMaxSize](https://wails.io/docs/reference/runtime/window#windowsetmaxsize)
|
||||
// Sets the maximum window size. Will resize the window if the window is currently larger than the given dimensions.
|
||||
// Setting a size of 0,0 will disable this constraint.
|
||||
export function WindowSetMaxSize(width: number, height: number): void;
|
||||
|
||||
// [WindowSetMinSize](https://wails.io/docs/reference/runtime/window#windowsetminsize)
|
||||
// Sets the minimum window size. Will resize the window if the window is currently smaller than the given dimensions.
|
||||
// Setting a size of 0,0 will disable this constraint.
|
||||
export function WindowSetMinSize(width: number, height: number): void;
|
||||
|
||||
// [WindowSetPosition](https://wails.io/docs/reference/runtime/window#windowsetposition)
|
||||
// Sets the window position relative to the monitor the window is currently on.
|
||||
export function WindowSetPosition(x: number, y: number): void;
|
||||
|
||||
// [WindowGetPosition](https://wails.io/docs/reference/runtime/window#windowgetposition)
|
||||
// Gets the window position relative to the monitor the window is currently on.
|
||||
export function WindowGetPosition(): Promise<Position>;
|
||||
|
||||
// [WindowHide](https://wails.io/docs/reference/runtime/window#windowhide)
|
||||
// Hides the window.
|
||||
export function WindowHide(): void;
|
||||
|
||||
// [WindowShow](https://wails.io/docs/reference/runtime/window#windowshow)
|
||||
// Shows the window, if it is currently hidden.
|
||||
export function WindowShow(): void;
|
||||
|
||||
// [WindowMaximise](https://wails.io/docs/reference/runtime/window#windowmaximise)
|
||||
// Maximises the window to fill the screen.
|
||||
export function WindowMaximise(): void;
|
||||
|
||||
// [WindowToggleMaximise](https://wails.io/docs/reference/runtime/window#windowtogglemaximise)
|
||||
// Toggles between Maximised and UnMaximised.
|
||||
export function WindowToggleMaximise(): void;
|
||||
|
||||
// [WindowUnmaximise](https://wails.io/docs/reference/runtime/window#windowunmaximise)
|
||||
// Restores the window to the dimensions and position prior to maximising.
|
||||
export function WindowUnmaximise(): void;
|
||||
|
||||
// [WindowIsMaximised](https://wails.io/docs/reference/runtime/window#windowismaximised)
|
||||
// Returns the state of the window, i.e. whether the window is maximised or not.
|
||||
export function WindowIsMaximised(): Promise<boolean>;
|
||||
|
||||
// [WindowMinimise](https://wails.io/docs/reference/runtime/window#windowminimise)
|
||||
// Minimises the window.
|
||||
export function WindowMinimise(): void;
|
||||
|
||||
// [WindowUnminimise](https://wails.io/docs/reference/runtime/window#windowunminimise)
|
||||
// Restores the window to the dimensions and position prior to minimising.
|
||||
export function WindowUnminimise(): void;
|
||||
|
||||
// [WindowIsMinimised](https://wails.io/docs/reference/runtime/window#windowisminimised)
|
||||
// Returns the state of the window, i.e. whether the window is minimised or not.
|
||||
export function WindowIsMinimised(): Promise<boolean>;
|
||||
|
||||
// [WindowIsNormal](https://wails.io/docs/reference/runtime/window#windowisnormal)
|
||||
// Returns the state of the window, i.e. whether the window is normal or not.
|
||||
export function WindowIsNormal(): Promise<boolean>;
|
||||
|
||||
// [WindowSetBackgroundColour](https://wails.io/docs/reference/runtime/window#windowsetbackgroundcolour)
|
||||
// Sets the background colour of the window to the given RGBA colour definition. This colour will show through for all transparent pixels.
|
||||
export function WindowSetBackgroundColour(R: number, G: number, B: number, A: number): void;
|
||||
|
||||
// [ScreenGetAll](https://wails.io/docs/reference/runtime/window#screengetall)
|
||||
// Gets the all screens. Call this anew each time you want to refresh data from the underlying windowing system.
|
||||
export function ScreenGetAll(): Promise<Screen[]>;
|
||||
|
||||
// [BrowserOpenURL](https://wails.io/docs/reference/runtime/browser#browseropenurl)
|
||||
// Opens the given URL in the system browser.
|
||||
export function BrowserOpenURL(url: string): void;
|
||||
|
||||
// [Environment](https://wails.io/docs/reference/runtime/intro#environment)
|
||||
// Returns information about the environment
|
||||
export function Environment(): Promise<EnvironmentInfo>;
|
||||
|
||||
// [Quit](https://wails.io/docs/reference/runtime/intro#quit)
|
||||
// Quits the application.
|
||||
export function Quit(): void;
|
||||
|
||||
// [Hide](https://wails.io/docs/reference/runtime/intro#hide)
|
||||
// Hides the application.
|
||||
export function Hide(): void;
|
||||
|
||||
// [Show](https://wails.io/docs/reference/runtime/intro#show)
|
||||
// Shows the application.
|
||||
export function Show(): void;
|
||||
|
||||
// [ClipboardGetText](https://wails.io/docs/reference/runtime/clipboard#clipboardgettext)
|
||||
// Returns the current text stored on clipboard
|
||||
export function ClipboardGetText(): Promise<string>;
|
||||
|
||||
// [ClipboardSetText](https://wails.io/docs/reference/runtime/clipboard#clipboardsettext)
|
||||
// Sets a text on the clipboard
|
||||
export function ClipboardSetText(text: string): Promise<boolean>;
|
||||
|
||||
// [OnFileDrop](https://wails.io/docs/reference/runtime/draganddrop#onfiledrop)
|
||||
// OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||
export function OnFileDrop(callback: (x: number, y: number ,paths: string[]) => void, useDropTarget: boolean) :void
|
||||
|
||||
// [OnFileDropOff](https://wails.io/docs/reference/runtime/draganddrop#dragandddropoff)
|
||||
// OnFileDropOff removes the drag and drop listeners and handlers.
|
||||
export function OnFileDropOff() :void
|
||||
|
||||
// Check if the file path resolver is available
|
||||
export function CanResolveFilePaths(): boolean;
|
||||
|
||||
// Resolves file paths for an array of files
|
||||
export function ResolveFilePaths(files: File[]): void
|
||||
@@ -0,0 +1,242 @@
|
||||
/*
|
||||
_ __ _ __
|
||||
| | / /___ _(_) /____
|
||||
| | /| / / __ `/ / / ___/
|
||||
| |/ |/ / /_/ / / (__ )
|
||||
|__/|__/\__,_/_/_/____/
|
||||
The electron alternative for Go
|
||||
(c) Lea Anthony 2019-present
|
||||
*/
|
||||
|
||||
export function LogPrint(message) {
|
||||
window.runtime.LogPrint(message);
|
||||
}
|
||||
|
||||
export function LogTrace(message) {
|
||||
window.runtime.LogTrace(message);
|
||||
}
|
||||
|
||||
export function LogDebug(message) {
|
||||
window.runtime.LogDebug(message);
|
||||
}
|
||||
|
||||
export function LogInfo(message) {
|
||||
window.runtime.LogInfo(message);
|
||||
}
|
||||
|
||||
export function LogWarning(message) {
|
||||
window.runtime.LogWarning(message);
|
||||
}
|
||||
|
||||
export function LogError(message) {
|
||||
window.runtime.LogError(message);
|
||||
}
|
||||
|
||||
export function LogFatal(message) {
|
||||
window.runtime.LogFatal(message);
|
||||
}
|
||||
|
||||
export function EventsOnMultiple(eventName, callback, maxCallbacks) {
|
||||
return window.runtime.EventsOnMultiple(eventName, callback, maxCallbacks);
|
||||
}
|
||||
|
||||
export function EventsOn(eventName, callback) {
|
||||
return EventsOnMultiple(eventName, callback, -1);
|
||||
}
|
||||
|
||||
export function EventsOff(eventName, ...additionalEventNames) {
|
||||
return window.runtime.EventsOff(eventName, ...additionalEventNames);
|
||||
}
|
||||
|
||||
export function EventsOffAll() {
|
||||
return window.runtime.EventsOffAll();
|
||||
}
|
||||
|
||||
export function EventsOnce(eventName, callback) {
|
||||
return EventsOnMultiple(eventName, callback, 1);
|
||||
}
|
||||
|
||||
export function EventsEmit(eventName) {
|
||||
let args = [eventName].slice.call(arguments);
|
||||
return window.runtime.EventsEmit.apply(null, args);
|
||||
}
|
||||
|
||||
export function WindowReload() {
|
||||
window.runtime.WindowReload();
|
||||
}
|
||||
|
||||
export function WindowReloadApp() {
|
||||
window.runtime.WindowReloadApp();
|
||||
}
|
||||
|
||||
export function WindowSetAlwaysOnTop(b) {
|
||||
window.runtime.WindowSetAlwaysOnTop(b);
|
||||
}
|
||||
|
||||
export function WindowSetSystemDefaultTheme() {
|
||||
window.runtime.WindowSetSystemDefaultTheme();
|
||||
}
|
||||
|
||||
export function WindowSetLightTheme() {
|
||||
window.runtime.WindowSetLightTheme();
|
||||
}
|
||||
|
||||
export function WindowSetDarkTheme() {
|
||||
window.runtime.WindowSetDarkTheme();
|
||||
}
|
||||
|
||||
export function WindowCenter() {
|
||||
window.runtime.WindowCenter();
|
||||
}
|
||||
|
||||
export function WindowSetTitle(title) {
|
||||
window.runtime.WindowSetTitle(title);
|
||||
}
|
||||
|
||||
export function WindowFullscreen() {
|
||||
window.runtime.WindowFullscreen();
|
||||
}
|
||||
|
||||
export function WindowUnfullscreen() {
|
||||
window.runtime.WindowUnfullscreen();
|
||||
}
|
||||
|
||||
export function WindowIsFullscreen() {
|
||||
return window.runtime.WindowIsFullscreen();
|
||||
}
|
||||
|
||||
export function WindowGetSize() {
|
||||
return window.runtime.WindowGetSize();
|
||||
}
|
||||
|
||||
export function WindowSetSize(width, height) {
|
||||
window.runtime.WindowSetSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetMaxSize(width, height) {
|
||||
window.runtime.WindowSetMaxSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetMinSize(width, height) {
|
||||
window.runtime.WindowSetMinSize(width, height);
|
||||
}
|
||||
|
||||
export function WindowSetPosition(x, y) {
|
||||
window.runtime.WindowSetPosition(x, y);
|
||||
}
|
||||
|
||||
export function WindowGetPosition() {
|
||||
return window.runtime.WindowGetPosition();
|
||||
}
|
||||
|
||||
export function WindowHide() {
|
||||
window.runtime.WindowHide();
|
||||
}
|
||||
|
||||
export function WindowShow() {
|
||||
window.runtime.WindowShow();
|
||||
}
|
||||
|
||||
export function WindowMaximise() {
|
||||
window.runtime.WindowMaximise();
|
||||
}
|
||||
|
||||
export function WindowToggleMaximise() {
|
||||
window.runtime.WindowToggleMaximise();
|
||||
}
|
||||
|
||||
export function WindowUnmaximise() {
|
||||
window.runtime.WindowUnmaximise();
|
||||
}
|
||||
|
||||
export function WindowIsMaximised() {
|
||||
return window.runtime.WindowIsMaximised();
|
||||
}
|
||||
|
||||
export function WindowMinimise() {
|
||||
window.runtime.WindowMinimise();
|
||||
}
|
||||
|
||||
export function WindowUnminimise() {
|
||||
window.runtime.WindowUnminimise();
|
||||
}
|
||||
|
||||
export function WindowSetBackgroundColour(R, G, B, A) {
|
||||
window.runtime.WindowSetBackgroundColour(R, G, B, A);
|
||||
}
|
||||
|
||||
export function ScreenGetAll() {
|
||||
return window.runtime.ScreenGetAll();
|
||||
}
|
||||
|
||||
export function WindowIsMinimised() {
|
||||
return window.runtime.WindowIsMinimised();
|
||||
}
|
||||
|
||||
export function WindowIsNormal() {
|
||||
return window.runtime.WindowIsNormal();
|
||||
}
|
||||
|
||||
export function BrowserOpenURL(url) {
|
||||
window.runtime.BrowserOpenURL(url);
|
||||
}
|
||||
|
||||
export function Environment() {
|
||||
return window.runtime.Environment();
|
||||
}
|
||||
|
||||
export function Quit() {
|
||||
window.runtime.Quit();
|
||||
}
|
||||
|
||||
export function Hide() {
|
||||
window.runtime.Hide();
|
||||
}
|
||||
|
||||
export function Show() {
|
||||
window.runtime.Show();
|
||||
}
|
||||
|
||||
export function ClipboardGetText() {
|
||||
return window.runtime.ClipboardGetText();
|
||||
}
|
||||
|
||||
export function ClipboardSetText(text) {
|
||||
return window.runtime.ClipboardSetText(text);
|
||||
}
|
||||
|
||||
/**
|
||||
* Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||
*
|
||||
* @export
|
||||
* @callback OnFileDropCallback
|
||||
* @param {number} x - x coordinate of the drop
|
||||
* @param {number} y - y coordinate of the drop
|
||||
* @param {string[]} paths - A list of file paths.
|
||||
*/
|
||||
|
||||
/**
|
||||
* OnFileDrop listens to drag and drop events and calls the callback with the coordinates of the drop and an array of path strings.
|
||||
*
|
||||
* @export
|
||||
* @param {OnFileDropCallback} callback - Callback for OnFileDrop returns a slice of file path strings when a drop is finished.
|
||||
* @param {boolean} [useDropTarget=true] - Only call the callback when the drop finished on an element that has the drop target style. (--wails-drop-target)
|
||||
*/
|
||||
export function OnFileDrop(callback, useDropTarget) {
|
||||
return window.runtime.OnFileDrop(callback, useDropTarget);
|
||||
}
|
||||
|
||||
/**
|
||||
* OnFileDropOff removes the drag and drop listeners and handlers.
|
||||
*/
|
||||
export function OnFileDropOff() {
|
||||
return window.runtime.OnFileDropOff();
|
||||
}
|
||||
|
||||
export function CanResolveFilePaths() {
|
||||
return window.runtime.CanResolveFilePaths();
|
||||
}
|
||||
|
||||
export function ResolveFilePaths(files) {
|
||||
return window.runtime.ResolveFilePaths(files);
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"target": "ES2022",
|
||||
"useDefineForClassFields": true,
|
||||
"lib": ["ES2022", "DOM", "DOM.Iterable"],
|
||||
"module": "ESNext",
|
||||
"skipLibCheck": true,
|
||||
"moduleResolution": "bundler",
|
||||
"allowImportingTsExtensions": true,
|
||||
"resolveJsonModule": true,
|
||||
"isolatedModules": true,
|
||||
"moduleDetection": "force",
|
||||
"noEmit": true,
|
||||
"jsx": "react-jsx",
|
||||
"strict": true,
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"noFallthroughCasesInSwitch": true,
|
||||
"baseUrl": ".",
|
||||
"paths": {
|
||||
"@/*": ["./src/*"],
|
||||
"@wails/*": ["./src/wailsjs/*"],
|
||||
"@fn_library": ["./src/declarations.d.ts"],
|
||||
"@graph": ["./src/declarations.d.ts"]
|
||||
}
|
||||
},
|
||||
"include": ["src"]
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{"root":["./src/App.tsx","./src/declarations.d.ts","./src/main.tsx","./src/types.ts","./src/components/AssertionPanel.tsx","./src/components/EntityDetail.tsx","./src/components/EntityDialog.tsx","./src/components/EntityTable.tsx","./src/components/GraphView.tsx","./src/components/ProjectSidebar.tsx","./src/components/RelationDialog.tsx","./src/components/RelationTable.tsx","./src/components/SearchBar.tsx","./src/lib/utils.ts","./src/wailsjs/go/models.ts","./src/wailsjs/go/main/App.d.ts","./src/wailsjs/runtime/runtime.d.ts"],"version":"5.9.3"}
|
||||
@@ -0,0 +1,20 @@
|
||||
import { defineConfig } from 'vite'
|
||||
import react from '@vitejs/plugin-react'
|
||||
import tailwindcss from '@tailwindcss/vite'
|
||||
import { resolve } from 'path'
|
||||
|
||||
export default defineConfig({
|
||||
plugins: [react(), tailwindcss()],
|
||||
resolve: {
|
||||
alias: {
|
||||
'@': resolve(__dirname, './src'),
|
||||
'@wails': resolve(__dirname, './src/wailsjs'),
|
||||
'@fn_library': resolve(__dirname, '../../../frontend/functions/ui'),
|
||||
'@graph': resolve(__dirname, '../../../frontend/functions/ui/graph'),
|
||||
},
|
||||
dedupe: ['react', 'react-dom'],
|
||||
},
|
||||
server: {
|
||||
port: 5174,
|
||||
},
|
||||
})
|
||||
@@ -0,0 +1,46 @@
|
||||
module fuzzygraph
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
fn-registry v0.0.0
|
||||
github.com/wailsapp/wails/v2 v2.11.0
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/bep/debounce v1.2.1 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e // indirect
|
||||
github.com/kr/pretty v0.3.1 // indirect
|
||||
github.com/labstack/echo/v4 v4.13.3 // indirect
|
||||
github.com/labstack/gommon v0.4.2 // indirect
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 // indirect
|
||||
github.com/leaanthony/gosod v1.0.4 // indirect
|
||||
github.com/leaanthony/slicer v1.6.0 // indirect
|
||||
github.com/leaanthony/u v1.1.1 // indirect
|
||||
github.com/mattn/go-colorable v0.1.13 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.37 // indirect
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/rivo/uniseg v0.4.7 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/samber/lo v1.49.1 // indirect
|
||||
github.com/stretchr/testify v1.11.1 // indirect
|
||||
github.com/tkrajina/go-reflector v0.5.8 // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fasttemplate v1.2.2 // indirect
|
||||
github.com/wailsapp/go-webview2 v1.0.22 // indirect
|
||||
github.com/wailsapp/mimetype v1.4.1 // indirect
|
||||
golang.org/x/crypto v0.48.0 // indirect
|
||||
golang.org/x/net v0.50.0 // indirect
|
||||
golang.org/x/sys v0.41.0 // indirect
|
||||
golang.org/x/text v0.34.0 // indirect
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
replace fn-registry => /home/lucas/fn_registry
|
||||
@@ -0,0 +1,98 @@
|
||||
github.com/bep/debounce v1.2.1 h1:v67fRdBA9UQu2NhLFXrSg0Brw7CexQekrBwDMM8bzeY=
|
||||
github.com/bep/debounce v1.2.1/go.mod h1:H8yggRPQKLUhUoqrJC1bO2xNya7vanpDl7xR3ISbCJ0=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e h1:Q3+PugElBCf4PFpxhErSzU3/PY5sFL5Z6rfv4AbGAck=
|
||||
github.com/jchv/go-winloader v0.0.0-20210711035445-715c2860da7e/go.mod h1:alcuEEnZsY1WQsagKhZDsoPCRoOijYqhZvPwLG0kzVs=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/labstack/echo/v4 v4.13.3 h1:pwhpCPrTl5qry5HRdM5FwdXnhXSLSY+WE+YQSeCaafY=
|
||||
github.com/labstack/echo/v4 v4.13.3/go.mod h1:o90YNEeQWjDozo584l7AwhJMHN0bOC4tAfg+Xox9q5g=
|
||||
github.com/labstack/gommon v0.4.2 h1:F8qTUNXgG1+6WQmqoUWnz8WiEU60mXVVw0P4ht1WRA0=
|
||||
github.com/labstack/gommon v0.4.2/go.mod h1:QlUFxVM+SNXhDL/Z7YhocGIBYOiwB0mXm1+1bAPHPyU=
|
||||
github.com/leaanthony/debme v1.2.1 h1:9Tgwf+kjcrbMQ4WnPcEIUcQuIZYqdWftzZkBr+i/oOc=
|
||||
github.com/leaanthony/debme v1.2.1/go.mod h1:3V+sCm5tYAgQymvSOfYQ5Xx2JCr+OXiD9Jkw3otUjiA=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1 h1:xd8bzARK3dErqkPFtoF9F3/HgN8UQk0ed1YDKpEz01A=
|
||||
github.com/leaanthony/go-ansi-parser v1.6.1/go.mod h1:+vva/2y4alzVmmIEpk9QDhA7vLC5zKDTRwfZGOp3IWU=
|
||||
github.com/leaanthony/gosod v1.0.4 h1:YLAbVyd591MRffDgxUOU1NwLhT9T1/YiwjKZpkNFeaI=
|
||||
github.com/leaanthony/gosod v1.0.4/go.mod h1:GKuIL0zzPj3O1SdWQOdgURSuhkF+Urizzxh26t9f1cw=
|
||||
github.com/leaanthony/slicer v1.6.0 h1:1RFP5uiPJvT93TAHi+ipd3NACobkW53yUiBqZheE/Js=
|
||||
github.com/leaanthony/slicer v1.6.0/go.mod h1:o/Iz29g7LN0GqH3aMjWAe90381nyZlDNquK+mtH2Fj8=
|
||||
github.com/leaanthony/u v1.1.1 h1:TUFjwDGlNX+WuwVEzDqQwC2lOv0P4uhTQw7CMFdiK7M=
|
||||
github.com/leaanthony/u v1.1.1/go.mod h1:9+o6hejoRljvZ3BzdYlVL0JYCwtnAsVuN9pVTQcaRfI=
|
||||
github.com/matryer/is v1.4.0/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/matryer/is v1.4.1 h1:55ehd8zaGABKLXQUe2awZ99BD/PTc2ls+KV/dXphgEQ=
|
||||
github.com/matryer/is v1.4.1/go.mod h1:8I/i5uYgLzgsgEloJE1U6xx5HkBQpAZvepWuujKwMRU=
|
||||
github.com/mattn/go-colorable v0.1.13 h1:fFA4WZxdEF4tXPZVKMLwD8oUnCTTo08duU7wxecdEvA=
|
||||
github.com/mattn/go-colorable v0.1.13/go.mod h1:7S9/ev0klgBDR4GtXTXX8a3vIGJpMovkB8vQcUbaXHg=
|
||||
github.com/mattn/go-isatty v0.0.16/go.mod h1:kYGgaQfpe5nmfYZH+SKPsOc2e4SrIfOl2e/yFXSvRLM=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.37 h1:3DOZp4cXis1cUIpCfXLtmlGolNLp2VEqhiB/PARNBIg=
|
||||
github.com/mattn/go-sqlite3 v1.14.37/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c h1:+mdjkGKdHQG3305AYmdv1U2eRNDiU2ErMBj1gwrq8eQ=
|
||||
github.com/pkg/browser v0.0.0-20240102092130-5ac0b6a4141c/go.mod h1:7rwL4CYBLnjLxUqIJNnCWiEdr3bn6IUYi15bNlnbCCU=
|
||||
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
||||
github.com/rogpeppe/go-internal v1.9.0/go.mod h1:WtVeX8xhTBvf0smdhujwtBcq4Qrzq/fJaraNFVN+nFs=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/samber/lo v1.49.1 h1:4BIFyVfuQSEpluc7Fua+j1NolZHiEHEpaSEKdsH0tew=
|
||||
github.com/samber/lo v1.49.1/go.mod h1:dO6KHFzUKXgP8LDhU0oI8d2hekjXnGOu0DB8Jecxd6o=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tkrajina/go-reflector v0.5.8 h1:yPADHrwmUbMq4RGEyaOUpz2H90sRsETNVpjzo3DLVQQ=
|
||||
github.com/tkrajina/go-reflector v0.5.8/go.mod h1:ECbqLgccecY5kPmPmXg1MrHW585yMcDkVl6IvJe64T4=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasttemplate v1.2.2 h1:lxLXG0uE3Qnshl9QyaK6XJxMXlQZELvChBOCmQD0Loo=
|
||||
github.com/valyala/fasttemplate v1.2.2/go.mod h1:KHLXt3tVN2HBp8eijSv/kGJopbvo7S+qRAEEKiv+SiQ=
|
||||
github.com/wailsapp/go-webview2 v1.0.22 h1:YT61F5lj+GGaat5OB96Aa3b4QA+mybD0Ggq6NZijQ58=
|
||||
github.com/wailsapp/go-webview2 v1.0.22/go.mod h1:qJmWAmAmaniuKGZPWwne+uor3AHMB5PFhqiK0Bbj8kc=
|
||||
github.com/wailsapp/mimetype v1.4.1 h1:pQN9ycO7uo4vsUUuPeHEYoUkLVkaRntMnHJxVwYhwHs=
|
||||
github.com/wailsapp/mimetype v1.4.1/go.mod h1:9aV5k31bBOv5z6u+QP8TltzvNGJPmNJD4XlAL3U+j3o=
|
||||
github.com/wailsapp/wails/v2 v2.11.0 h1:seLacV8pqupq32IjS4Y7V8ucab0WZwtK6VvUVxSBtqQ=
|
||||
github.com/wailsapp/wails/v2 v2.11.0/go.mod h1:jrf0ZaM6+GBc1wRmXsM8cIvzlg0karYin3erahI4+0k=
|
||||
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||
golang.org/x/net v0.0.0-20210505024714-0287a6fb4125/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60=
|
||||
golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM=
|
||||
golang.org/x/sys v0.0.0-20200810151505-1b9f1253b3ed/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
|
||||
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
@@ -0,0 +1,183 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand/v2"
|
||||
|
||||
ops "fn-registry/fn_operations"
|
||||
)
|
||||
|
||||
// buildGraphData converts entities and relations from operations.db into sigma.js-compatible GraphData.
|
||||
func buildGraphData(db *ops.DB) (GraphData, error) {
|
||||
entities, err := db.ListEntities("", "")
|
||||
if err != nil {
|
||||
return GraphData{}, fmt.Errorf("listing entities: %w", err)
|
||||
}
|
||||
|
||||
relations, err := db.ListRelations("")
|
||||
if err != nil {
|
||||
return GraphData{}, fmt.Errorf("listing relations: %w", err)
|
||||
}
|
||||
|
||||
// Compute degree per node
|
||||
degree := map[string]int{}
|
||||
for _, r := range relations {
|
||||
degree[r.FromEntity]++
|
||||
degree[r.ToEntity]++
|
||||
}
|
||||
|
||||
// Build nodes
|
||||
nodes := make([]GraphNode, 0, len(entities))
|
||||
for _, e := range entities {
|
||||
color, ok := entityTypeColors[e.TypeRef]
|
||||
if !ok {
|
||||
color = "#95a5a6" // default grey
|
||||
}
|
||||
|
||||
// Size: base + degree factor + risk_score factor
|
||||
size := 8.0
|
||||
size += math.Min(float64(degree[e.ID])*2.0, 20.0)
|
||||
if rs, ok := e.Metadata["risk_score"]; ok {
|
||||
if v, ok := toFloat64(rs); ok {
|
||||
size += v * 0.1 // risk_score contributes up to ~10
|
||||
}
|
||||
}
|
||||
|
||||
nodes = append(nodes, GraphNode{
|
||||
ID: e.ID,
|
||||
Label: e.Name,
|
||||
Type: e.TypeRef,
|
||||
Color: color,
|
||||
Size: size,
|
||||
X: rand.Float64()*100 - 50,
|
||||
Y: rand.Float64()*100 - 50,
|
||||
Extra: e.Metadata,
|
||||
})
|
||||
}
|
||||
|
||||
// Build edges
|
||||
edges := make([]GraphEdge, 0, len(relations))
|
||||
for _, r := range relations {
|
||||
w := 1.0
|
||||
if r.Weight != nil {
|
||||
w = *r.Weight
|
||||
}
|
||||
|
||||
edges = append(edges, GraphEdge{
|
||||
ID: r.ID,
|
||||
Source: r.FromEntity,
|
||||
Target: r.ToEntity,
|
||||
Label: r.Name,
|
||||
Color: "#ffffff30",
|
||||
Size: math.Max(w*3, 0.5),
|
||||
Type: "arrow",
|
||||
})
|
||||
}
|
||||
|
||||
return GraphData{Nodes: nodes, Edges: edges}, nil
|
||||
}
|
||||
|
||||
// buildEgoGraph returns a subgraph centered on entityID up to depth hops.
|
||||
func buildEgoGraph(db *ops.DB, entityID string, depth int) (GraphData, error) {
|
||||
if depth < 1 {
|
||||
depth = 1
|
||||
}
|
||||
if depth > 5 {
|
||||
depth = 5
|
||||
}
|
||||
|
||||
relations, err := db.ListRelations("")
|
||||
if err != nil {
|
||||
return GraphData{}, err
|
||||
}
|
||||
|
||||
// BFS to collect entity IDs within depth
|
||||
visited := map[string]bool{entityID: true}
|
||||
frontier := []string{entityID}
|
||||
for d := 0; d < depth; d++ {
|
||||
var next []string
|
||||
for _, id := range frontier {
|
||||
for _, r := range relations {
|
||||
if r.FromEntity == id && !visited[r.ToEntity] {
|
||||
visited[r.ToEntity] = true
|
||||
next = append(next, r.ToEntity)
|
||||
}
|
||||
if r.ToEntity == id && !visited[r.FromEntity] {
|
||||
visited[r.FromEntity] = true
|
||||
next = append(next, r.FromEntity)
|
||||
}
|
||||
}
|
||||
}
|
||||
frontier = next
|
||||
}
|
||||
|
||||
// Filter entities and relations to subgraph
|
||||
entities, err := db.ListEntities("", "")
|
||||
if err != nil {
|
||||
return GraphData{}, err
|
||||
}
|
||||
|
||||
degree := map[string]int{}
|
||||
var subRels []ops.Relation
|
||||
for _, r := range relations {
|
||||
if visited[r.FromEntity] && visited[r.ToEntity] {
|
||||
subRels = append(subRels, r)
|
||||
degree[r.FromEntity]++
|
||||
degree[r.ToEntity]++
|
||||
}
|
||||
}
|
||||
|
||||
nodes := make([]GraphNode, 0)
|
||||
for _, e := range entities {
|
||||
if !visited[e.ID] {
|
||||
continue
|
||||
}
|
||||
color, ok := entityTypeColors[e.TypeRef]
|
||||
if !ok {
|
||||
color = "#95a5a6"
|
||||
}
|
||||
size := 8.0 + math.Min(float64(degree[e.ID])*2.0, 20.0)
|
||||
if rs, ok := e.Metadata["risk_score"]; ok {
|
||||
if v, ok := toFloat64(rs); ok {
|
||||
size += v * 0.1
|
||||
}
|
||||
}
|
||||
nodes = append(nodes, GraphNode{
|
||||
ID: e.ID, Label: e.Name, Type: e.TypeRef,
|
||||
Color: color, Size: size,
|
||||
X: rand.Float64()*100 - 50, Y: rand.Float64()*100 - 50,
|
||||
Extra: e.Metadata,
|
||||
})
|
||||
}
|
||||
|
||||
edges := make([]GraphEdge, 0)
|
||||
for _, r := range subRels {
|
||||
w := 1.0
|
||||
if r.Weight != nil {
|
||||
w = *r.Weight
|
||||
}
|
||||
edges = append(edges, GraphEdge{
|
||||
ID: r.ID, Source: r.FromEntity, Target: r.ToEntity,
|
||||
Label: r.Name, Color: "#ffffff30",
|
||||
Size: math.Max(w*3, 0.5), Type: "arrow",
|
||||
})
|
||||
}
|
||||
|
||||
return GraphData{Nodes: nodes, Edges: edges}, nil
|
||||
}
|
||||
|
||||
func toFloat64(v any) (float64, bool) {
|
||||
switch n := v.(type) {
|
||||
case float64:
|
||||
return n, true
|
||||
case float32:
|
||||
return float64(n), true
|
||||
case int:
|
||||
return float64(n), true
|
||||
case int64:
|
||||
return float64(n), true
|
||||
default:
|
||||
return 0, false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,83 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"embed"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/wailsapp/wails/v2"
|
||||
"github.com/wailsapp/wails/v2/pkg/options"
|
||||
"github.com/wailsapp/wails/v2/pkg/options/assetserver"
|
||||
)
|
||||
|
||||
//go:embed all:frontend/dist
|
||||
var assets embed.FS
|
||||
|
||||
func main() {
|
||||
// File logger
|
||||
logFile, err := os.OpenFile("fuzzygraph.log", os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0644)
|
||||
if err == nil {
|
||||
log.SetOutput(logFile)
|
||||
defer logFile.Close()
|
||||
}
|
||||
log.SetFlags(log.Ltime | log.Lmicroseconds | log.Lshortfile)
|
||||
|
||||
log.Println("=== fuzzygraph starting ===")
|
||||
log.Printf("cwd: %s", func() string { d, _ := os.Getwd(); return d }())
|
||||
log.Printf("os.Args: %v", os.Args)
|
||||
|
||||
// Resolve projects directory relative to cwd
|
||||
projectsDir := "projects"
|
||||
if cwd, err := os.Getwd(); err == nil {
|
||||
absProjects := filepath.Join(cwd, "projects")
|
||||
if _, err := os.Stat(absProjects); err == nil {
|
||||
projectsDir = absProjects
|
||||
log.Printf("projectsDir (exists): %s", projectsDir)
|
||||
} else {
|
||||
// Create it
|
||||
os.MkdirAll(absProjects, 0o755)
|
||||
projectsDir = absProjects
|
||||
log.Printf("projectsDir (created): %s", projectsDir)
|
||||
}
|
||||
}
|
||||
|
||||
// Resolve registry root
|
||||
registryRoot := os.Getenv("FN_REGISTRY_ROOT")
|
||||
if registryRoot == "" {
|
||||
if cwd, err := os.Getwd(); err == nil {
|
||||
registryRoot = filepath.Join(cwd, "..", "..")
|
||||
}
|
||||
}
|
||||
log.Printf("registryRoot: %s", registryRoot)
|
||||
log.Printf("registry.db exists: %v", func() bool {
|
||||
_, err := os.Stat(filepath.Join(registryRoot, "registry.db"))
|
||||
return err == nil
|
||||
}())
|
||||
|
||||
app := NewApp(projectsDir, registryRoot)
|
||||
log.Println("App created, starting Wails...")
|
||||
|
||||
runErr := wails.Run(&options.App{
|
||||
Title: "FuzzyGraph — OSINT Intelligence",
|
||||
Width: 1400,
|
||||
Height: 900,
|
||||
AssetServer: &assetserver.Options{
|
||||
Assets: assets,
|
||||
},
|
||||
BackgroundColour: &options.RGBA{R: 10, G: 10, B: 15, A: 1},
|
||||
OnStartup: app.startup,
|
||||
OnShutdown: app.shutdown,
|
||||
Bind: []interface{}{
|
||||
app,
|
||||
},
|
||||
})
|
||||
|
||||
if runErr != nil {
|
||||
log.Printf("ERROR wails.Run: %v", runErr)
|
||||
fmt.Fprintf(os.Stderr, "error: %v\n", runErr)
|
||||
os.Exit(1)
|
||||
}
|
||||
log.Println("=== fuzzygraph exited cleanly ===")
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
ops "fn-registry/fn_operations"
|
||||
)
|
||||
|
||||
// listProjectDirs returns subdirectories under projectsDir.
|
||||
func listProjectDirs(projectsDir string) ([]ProjectInfo, error) {
|
||||
log.Printf("[listProjectDirs] scanning %s", projectsDir)
|
||||
|
||||
entries, err := os.ReadDir(projectsDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
log.Printf("[listProjectDirs] dir does not exist, returning nil")
|
||||
return nil, nil
|
||||
}
|
||||
log.Printf("[listProjectDirs] ERROR reading dir: %v", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
log.Printf("[listProjectDirs] found %d entries", len(entries))
|
||||
|
||||
var projects []ProjectInfo
|
||||
for _, e := range entries {
|
||||
log.Printf("[listProjectDirs] entry: %s isDir=%v", e.Name(), e.IsDir())
|
||||
if !e.IsDir() {
|
||||
continue
|
||||
}
|
||||
dbPath := filepath.Join(projectsDir, e.Name(), "operations.db")
|
||||
if _, err := os.Stat(dbPath); err != nil {
|
||||
log.Printf("[listProjectDirs] %s: no operations.db, skipping", e.Name())
|
||||
continue
|
||||
}
|
||||
|
||||
info := ProjectInfo{Name: e.Name()}
|
||||
|
||||
// Quick counts from the DB
|
||||
db, err := ops.Open(dbPath)
|
||||
if err == nil {
|
||||
entities, _ := db.ListEntities("", "")
|
||||
relations, _ := db.ListRelations("")
|
||||
info.EntityCount = len(entities)
|
||||
info.RelCount = len(relations)
|
||||
db.Close()
|
||||
log.Printf("[listProjectDirs] %s: OK (entities=%d relations=%d)", e.Name(), info.EntityCount, info.RelCount)
|
||||
} else {
|
||||
log.Printf("[listProjectDirs] %s: ERROR opening db: %v", e.Name(), err)
|
||||
}
|
||||
|
||||
projects = append(projects, info)
|
||||
}
|
||||
|
||||
log.Printf("[listProjectDirs] returning %d projects", len(projects))
|
||||
return projects, nil
|
||||
}
|
||||
|
||||
// createProject creates a new project directory with an operations.db.
|
||||
func createProject(projectsDir, name string) error {
|
||||
dir := filepath.Join(projectsDir, name)
|
||||
log.Printf("[createProject] creating dir: %s", dir)
|
||||
|
||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
||||
log.Printf("[createProject] ERROR mkdir: %v", err)
|
||||
return fmt.Errorf("creating project directory: %w", err)
|
||||
}
|
||||
|
||||
dbPath := filepath.Join(dir, "operations.db")
|
||||
log.Printf("[createProject] creating db: %s", dbPath)
|
||||
|
||||
db, err := ops.Open(dbPath)
|
||||
if err != nil {
|
||||
log.Printf("[createProject] ERROR opening db: %v", err)
|
||||
return fmt.Errorf("creating operations.db: %w", err)
|
||||
}
|
||||
|
||||
if err := db.Close(); err != nil {
|
||||
log.Printf("[createProject] ERROR closing db: %v", err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Verify
|
||||
if fi, err := os.Stat(dbPath); err != nil {
|
||||
log.Printf("[createProject] WARNING: db not found after creation: %v", err)
|
||||
} else {
|
||||
log.Printf("[createProject] OK: db created, size=%d bytes", fi.Size())
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// deleteProject removes a project directory and its operations.db.
|
||||
func deleteProject(projectsDir, name string) error {
|
||||
dir := filepath.Join(projectsDir, name)
|
||||
log.Printf("[deleteProject] removing: %s", dir)
|
||||
return os.RemoveAll(dir)
|
||||
}
|
||||
@@ -0,0 +1,106 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math"
|
||||
"math/rand/v2"
|
||||
|
||||
ops "fn-registry/fn_operations"
|
||||
)
|
||||
|
||||
// searchEntitiesFTS searches entities using FTS and returns matching entities.
|
||||
func searchEntitiesFTS(db *ops.DB, query string) ([]ops.Entity, error) {
|
||||
if query == "" {
|
||||
return db.ListEntities("", "")
|
||||
}
|
||||
return db.SearchEntities(query, "")
|
||||
}
|
||||
|
||||
// searchGraph returns a GraphData subgraph containing matching entities and their direct relations.
|
||||
func searchGraph(db *ops.DB, query string) (GraphData, error) {
|
||||
matches, err := searchEntitiesFTS(db, query)
|
||||
if err != nil {
|
||||
return GraphData{}, fmt.Errorf("searching entities: %w", err)
|
||||
}
|
||||
|
||||
if len(matches) == 0 {
|
||||
return GraphData{}, nil
|
||||
}
|
||||
|
||||
matchIDs := map[string]bool{}
|
||||
for _, e := range matches {
|
||||
matchIDs[e.ID] = true
|
||||
}
|
||||
|
||||
// Get all relations and filter to those touching matched entities
|
||||
allRelations, err := db.ListRelations("")
|
||||
if err != nil {
|
||||
return GraphData{}, err
|
||||
}
|
||||
|
||||
// Collect neighbor IDs
|
||||
neighborIDs := map[string]bool{}
|
||||
var subRels []ops.Relation
|
||||
for _, r := range allRelations {
|
||||
if matchIDs[r.FromEntity] || matchIDs[r.ToEntity] {
|
||||
subRels = append(subRels, r)
|
||||
neighborIDs[r.FromEntity] = true
|
||||
neighborIDs[r.ToEntity] = true
|
||||
}
|
||||
}
|
||||
|
||||
// Merge match IDs and neighbor IDs
|
||||
for id := range matchIDs {
|
||||
neighborIDs[id] = true
|
||||
}
|
||||
|
||||
// Get all entities in the subgraph
|
||||
allEntities, err := db.ListEntities("", "")
|
||||
if err != nil {
|
||||
return GraphData{}, err
|
||||
}
|
||||
|
||||
degree := map[string]int{}
|
||||
for _, r := range subRels {
|
||||
degree[r.FromEntity]++
|
||||
degree[r.ToEntity]++
|
||||
}
|
||||
|
||||
nodes := make([]GraphNode, 0)
|
||||
for _, e := range allEntities {
|
||||
if !neighborIDs[e.ID] {
|
||||
continue
|
||||
}
|
||||
color, ok := entityTypeColors[e.TypeRef]
|
||||
if !ok {
|
||||
color = "#95a5a6"
|
||||
}
|
||||
size := 8.0 + math.Min(float64(degree[e.ID])*2.0, 20.0)
|
||||
if rs, ok := e.Metadata["risk_score"]; ok {
|
||||
if v, ok := toFloat64(rs); ok {
|
||||
size += v * 0.1
|
||||
}
|
||||
}
|
||||
nodes = append(nodes, GraphNode{
|
||||
ID: e.ID, Label: e.Name, Type: e.TypeRef,
|
||||
Color: color, Size: size,
|
||||
X: rand.Float64()*100 - 50, Y: rand.Float64()*100 - 50,
|
||||
Extra: e.Metadata,
|
||||
})
|
||||
}
|
||||
|
||||
edges := make([]GraphEdge, 0)
|
||||
for _, r := range subRels {
|
||||
w := 1.0
|
||||
if r.Weight != nil {
|
||||
w = *r.Weight
|
||||
}
|
||||
edges = append(edges, GraphEdge{
|
||||
ID: r.ID, Source: r.FromEntity, Target: r.ToEntity,
|
||||
Label: r.Name, Color: "#ffffff30",
|
||||
Size: math.Max(w*3, 0.5), Type: "arrow",
|
||||
})
|
||||
}
|
||||
|
||||
return GraphData{Nodes: nodes, Edges: edges}, nil
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package main
|
||||
|
||||
// EntityTypePreset defines an OSINT entity type with its visual properties and suggested metadata fields.
|
||||
type EntityTypePreset struct {
|
||||
TypeRef string `json:"type_ref"` // registry type ID
|
||||
Label string `json:"label"` // human-readable
|
||||
Color string `json:"color"` // hex color for graph nodes
|
||||
MetadataFields []string `json:"metadata_fields"` // suggested metadata keys
|
||||
}
|
||||
|
||||
// GraphData matches the frontend-lib GraphData interface for sigma.js rendering.
|
||||
type GraphData struct {
|
||||
Nodes []GraphNode `json:"nodes"`
|
||||
Edges []GraphEdge `json:"edges"`
|
||||
}
|
||||
|
||||
// GraphNode matches the frontend-lib GraphNode interface.
|
||||
type GraphNode struct {
|
||||
ID string `json:"id"`
|
||||
Label string `json:"label"`
|
||||
Type string `json:"type"`
|
||||
Color string `json:"color"`
|
||||
Size float64 `json:"size"`
|
||||
X float64 `json:"x"`
|
||||
Y float64 `json:"y"`
|
||||
// Extra fields from entity metadata (flattened)
|
||||
Extra map[string]any `json:"extra,omitempty"`
|
||||
}
|
||||
|
||||
// GraphEdge matches the frontend-lib GraphEdge interface.
|
||||
type GraphEdge struct {
|
||||
ID string `json:"id"`
|
||||
Source string `json:"source"`
|
||||
Target string `json:"target"`
|
||||
Label string `json:"label"`
|
||||
Color string `json:"color"`
|
||||
Size float64 `json:"size"`
|
||||
Type string `json:"type"` // "arrow"
|
||||
}
|
||||
|
||||
// ProjectInfo describes a project directory.
|
||||
type ProjectInfo struct {
|
||||
Name string `json:"name"`
|
||||
Description string `json:"description"`
|
||||
EntityCount int `json:"entity_count"`
|
||||
RelCount int `json:"relation_count"`
|
||||
}
|
||||
|
||||
// EntityInput is the DTO for creating/updating entities from the frontend.
|
||||
type EntityInput struct {
|
||||
Name string `json:"name"`
|
||||
TypeRef string `json:"type_ref"`
|
||||
Description string `json:"description"`
|
||||
Tags []string `json:"tags"`
|
||||
Metadata map[string]any `json:"metadata"`
|
||||
Notes string `json:"notes"`
|
||||
}
|
||||
|
||||
// RelationInput is the DTO for creating/updating relations from the frontend.
|
||||
type RelationInputDTO struct {
|
||||
Name string `json:"name"`
|
||||
FromEntity string `json:"from_entity"`
|
||||
ToEntity string `json:"to_entity"`
|
||||
Description string `json:"description"`
|
||||
Weight *float64 `json:"weight"`
|
||||
Tags []string `json:"tags"`
|
||||
Notes string `json:"notes"`
|
||||
}
|
||||
|
||||
// AssertionInput is the DTO for creating assertions from the frontend.
|
||||
type AssertionInput struct {
|
||||
EntityID string `json:"entity_id"`
|
||||
Name string `json:"name"`
|
||||
Kind string `json:"kind"`
|
||||
Rule string `json:"rule"`
|
||||
Severity string `json:"severity"`
|
||||
Description string `json:"description"`
|
||||
}
|
||||
|
||||
// Color map for OSINT entity types
|
||||
var entityTypeColors = map[string]string{
|
||||
"osint_person_go_cybersecurity": "#e74c3c",
|
||||
"osint_organization_go_cybersecurity": "#3498db",
|
||||
"osint_crypto_wallet_go_cybersecurity": "#f39c12",
|
||||
"osint_ip_address_go_cybersecurity": "#2ecc71",
|
||||
"osint_domain_go_cybersecurity": "#9b59b6",
|
||||
"osint_email_go_cybersecurity": "#1abc9c",
|
||||
"osint_phone_go_cybersecurity": "#e67e22",
|
||||
"osint_malware_go_cybersecurity": "#c0392b",
|
||||
"osint_vulnerability_go_cybersecurity": "#8e44ad",
|
||||
"osint_social_media_go_cybersecurity": "#2980b9",
|
||||
"osint_document_go_cybersecurity": "#7f8c8d",
|
||||
"osint_event_go_cybersecurity": "#d35400",
|
||||
"osint_location_go_cybersecurity": "#27ae60",
|
||||
}
|
||||
|
||||
var entityTypePresets = []EntityTypePreset{
|
||||
{"osint_person_go_cybersecurity", "Person", "#e74c3c", []string{"full_name", "alias", "nationality", "dob", "gender", "risk_score"}},
|
||||
{"osint_organization_go_cybersecurity", "Organization", "#3498db", []string{"legal_name", "country", "sector", "founded", "risk_score"}},
|
||||
{"osint_crypto_wallet_go_cybersecurity", "Crypto Wallet", "#f39c12", []string{"address", "blockchain", "balance", "first_seen", "last_seen"}},
|
||||
{"osint_ip_address_go_cybersecurity", "IP Address", "#2ecc71", []string{"ip", "asn", "country", "isp", "geolocation", "last_seen"}},
|
||||
{"osint_domain_go_cybersecurity", "Domain", "#9b59b6", []string{"fqdn", "registrar", "created_date", "expires_date", "name_servers"}},
|
||||
{"osint_email_go_cybersecurity", "Email", "#1abc9c", []string{"address", "provider", "verified", "breached"}},
|
||||
{"osint_phone_go_cybersecurity", "Phone", "#e67e22", []string{"number", "country_code", "carrier", "phone_type"}},
|
||||
{"osint_malware_go_cybersecurity", "Malware", "#c0392b", []string{"family", "hash_sha256", "first_seen", "last_seen", "threat_level"}},
|
||||
{"osint_vulnerability_go_cybersecurity", "Vulnerability", "#8e44ad", []string{"cve_id", "cvss", "affected_product", "published", "exploited"}},
|
||||
{"osint_social_media_go_cybersecurity", "Social Media", "#2980b9", []string{"platform", "username", "url", "followers", "verified"}},
|
||||
{"osint_document_go_cybersecurity", "Document", "#7f8c8d", []string{"title", "format", "classification", "hash_sha256", "source"}},
|
||||
{"osint_event_go_cybersecurity", "Event", "#d35400", []string{"event_type", "date", "location", "description", "severity"}},
|
||||
{"osint_location_go_cybersecurity", "Location", "#27ae60", []string{"lat", "lon", "address", "country", "city"}},
|
||||
}
|
||||
|
||||
var relationPresets = []string{
|
||||
"funds", "employs", "communicates_with", "owns", "operates",
|
||||
"controls", "affiliated_with", "located_at", "resolves_to",
|
||||
"registered_by", "hosts", "exploits", "attributed_to", "related_to",
|
||||
}
|
||||
+14
@@ -0,0 +1,14 @@
|
||||
{
|
||||
"$schema": "https://wails.io/schemas/config.v2.json",
|
||||
"name": "fuzzygraph",
|
||||
"outputfilename": "fuzzygraph",
|
||||
"frontend:dir": "./frontend",
|
||||
"frontend:install": "pnpm install",
|
||||
"frontend:build": "pnpm run build",
|
||||
"frontend:dev:watcher": "pnpm run dev",
|
||||
"frontend:dev:serverUrl": "auto",
|
||||
"wailsjsdir": "./frontend/src",
|
||||
"author": {
|
||||
"name": "Egutierrez"
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user