archsight 0.1.4 → 0.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/README.md +33 -0
- data/chart/archsight/Chart.yaml +6 -0
- data/chart/archsight/README.md +160 -0
- data/chart/archsight/templates/NOTES.txt +22 -0
- data/chart/archsight/templates/_helpers.tpl +62 -0
- data/chart/archsight/templates/deployment.yaml +114 -0
- data/chart/archsight/templates/ingress.yaml +56 -0
- data/chart/archsight/templates/resources-configmap.yaml +10 -0
- data/chart/archsight/templates/resources-pvc.yaml +23 -0
- data/chart/archsight/templates/service.yaml +15 -0
- data/chart/archsight/templates/serviceaccount.yaml +12 -0
- data/chart/archsight/values.yaml +162 -0
- data/lib/archsight/analysis/executor.rb +0 -10
- data/lib/archsight/annotations/annotation.rb +85 -36
- data/lib/archsight/annotations/architecture_annotations.rb +1 -34
- data/lib/archsight/annotations/computed.rb +1 -1
- data/lib/archsight/annotations/generated_annotations.rb +6 -3
- data/lib/archsight/annotations/git_annotations.rb +8 -4
- data/lib/archsight/annotations/interface_annotations.rb +35 -0
- data/lib/archsight/cli.rb +3 -1
- data/lib/archsight/editor/content_hasher.rb +37 -0
- data/lib/archsight/editor/file_writer.rb +79 -0
- data/lib/archsight/editor.rb +237 -0
- data/lib/archsight/import/handlers/github.rb +14 -6
- data/lib/archsight/import/handlers/gitlab.rb +14 -6
- data/lib/archsight/import/handlers/repository.rb +3 -1
- data/lib/archsight/import/team_matcher.rb +111 -61
- data/lib/archsight/mcp/execute_analysis_tool.rb +100 -0
- data/lib/archsight/mcp.rb +1 -0
- data/lib/archsight/resources/analysis.rb +1 -17
- data/lib/archsight/resources/application_interface.rb +1 -5
- data/lib/archsight/resources/base.rb +14 -14
- data/lib/archsight/resources/business_actor.rb +1 -1
- data/lib/archsight/resources/technology_interface.rb +1 -1
- data/lib/archsight/resources/technology_service.rb +5 -0
- data/lib/archsight/version.rb +1 -1
- data/lib/archsight/web/application.rb +8 -0
- data/lib/archsight/web/doc/import.md +10 -2
- data/lib/archsight/web/editor/form_builder.rb +100 -0
- data/lib/archsight/web/editor/routes.rb +293 -0
- data/lib/archsight/web/public/css/editor.css +863 -0
- data/lib/archsight/web/public/css/instance.css +6 -0
- data/lib/archsight/web/public/js/editor.js +421 -0
- data/lib/archsight/web/public/js/lexical-editor.js +308 -0
- data/lib/archsight/web/views/partials/editor/_field.haml +80 -0
- data/lib/archsight/web/views/partials/editor/_form.haml +131 -0
- data/lib/archsight/web/views/partials/editor/_relations.haml +39 -0
- data/lib/archsight/web/views/partials/editor/_yaml_output.haml +33 -0
- data/lib/archsight/web/views/partials/instance/_analysis_detail.haml +4 -11
- data/lib/archsight/web/views/partials/instance/_detail.haml +4 -0
- data/lib/archsight/web/views/partials/layout/_content.haml +8 -2
- data/lib/archsight/web/views/partials/layout/_head.haml +2 -0
- metadata +26 -1
|
@@ -51,6 +51,7 @@ class Archsight::Resources::Analysis < Archsight::Resources::Base
|
|
|
51
51
|
annotation "analysis/script",
|
|
52
52
|
description: "Ruby script to execute in sandboxed environment",
|
|
53
53
|
title: "Script",
|
|
54
|
+
format: :ruby,
|
|
54
55
|
sidebar: false
|
|
55
56
|
|
|
56
57
|
# Description
|
|
@@ -63,23 +64,6 @@ class Archsight::Resources::Analysis < Archsight::Resources::Base
|
|
|
63
64
|
description: "Maximum execution time (e.g., '30s', '5m')",
|
|
64
65
|
title: "Timeout"
|
|
65
66
|
|
|
66
|
-
# Output configuration
|
|
67
|
-
annotation "analysis/output",
|
|
68
|
-
description: "Output mode for results",
|
|
69
|
-
title: "Output",
|
|
70
|
-
enum: %w[console file]
|
|
71
|
-
|
|
72
|
-
annotation "analysis/outputPath",
|
|
73
|
-
description: "File path for output (when output mode is 'file')",
|
|
74
|
-
title: "Output Path",
|
|
75
|
-
sidebar: false
|
|
76
|
-
|
|
77
|
-
# Enabled flag
|
|
78
|
-
annotation "analysis/enabled",
|
|
79
|
-
description: "Whether this analysis is enabled",
|
|
80
|
-
title: "Enabled",
|
|
81
|
-
enum: %w[true false]
|
|
82
|
-
|
|
83
67
|
# Pattern annotation for custom configuration
|
|
84
68
|
annotation "analysis/config/*",
|
|
85
69
|
description: "Custom configuration values for the analysis script",
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# ApplicationInterface between ApplicationComponent
|
|
4
4
|
class Archsight::Resources::ApplicationInterface < Archsight::Resources::Base
|
|
5
|
-
include_annotations :git, :architecture, :generated
|
|
5
|
+
include_annotations :git, :architecture, :interface, :generated
|
|
6
6
|
|
|
7
7
|
description <<~MD
|
|
8
8
|
Represents a point of access where application services are made available.
|
|
@@ -39,10 +39,6 @@ class Archsight::Resources::ApplicationInterface < Archsight::Resources::Base
|
|
|
39
39
|
description: "API authentication method",
|
|
40
40
|
title: "API Authentication Method",
|
|
41
41
|
enum: ["none", "hard coded", "token", "oidc"]
|
|
42
|
-
annotation "api/authenticationProvider",
|
|
43
|
-
description: "API authentication provider",
|
|
44
|
-
title: "API Authentication Provider",
|
|
45
|
-
enum: %w[eiam cloud custom]
|
|
46
42
|
annotation "api/authorization",
|
|
47
43
|
description: "API authorization mechanism",
|
|
48
44
|
title: "API Authorization",
|
|
@@ -15,7 +15,7 @@ module Archsight
|
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def self.relation(verb, kind, klass_name)
|
|
18
|
-
@relations ||= []
|
|
18
|
+
@relations ||= [] #: Array[[Symbol, Symbol, String]]
|
|
19
19
|
@relations << [verb, kind, klass_name]
|
|
20
20
|
end
|
|
21
21
|
|
|
@@ -25,10 +25,10 @@ module Archsight
|
|
|
25
25
|
|
|
26
26
|
# Define an annotation using the Annotation class
|
|
27
27
|
def self.annotation(key, description: nil, filter: nil, title: nil, format: nil, enum: nil, sidebar: true,
|
|
28
|
-
type: nil, list: false)
|
|
29
|
-
@annotations ||= []
|
|
28
|
+
type: nil, list: false, editor: true)
|
|
29
|
+
@annotations ||= [] #: Array[Archsight::Annotations::Annotation]
|
|
30
30
|
options = { description: description, filter: filter, title: title, format: format, enum: enum,
|
|
31
|
-
sidebar: sidebar, type: type, list: list }
|
|
31
|
+
sidebar: sidebar, type: type, list: list, editor: editor }
|
|
32
32
|
@annotations << Archsight::Annotations::Annotation.new(key, options)
|
|
33
33
|
end
|
|
34
34
|
|
|
@@ -51,15 +51,15 @@ module Archsight
|
|
|
51
51
|
# @param list [Boolean] Whether values are lists (default false)
|
|
52
52
|
# @yield Block that computes the annotation value, evaluated in Evaluator context
|
|
53
53
|
def self.computed_annotation(key, description: nil, filter: nil, title: nil, format: nil, enum: nil,
|
|
54
|
-
sidebar: false, type: nil, list: false, &)
|
|
54
|
+
sidebar: false, type: nil, list: false, editor: true, &)
|
|
55
55
|
require_relative "../annotations/computed"
|
|
56
|
-
@computed_annotations ||= []
|
|
56
|
+
@computed_annotations ||= [] #: Array[Archsight::Annotations::Computed]
|
|
57
57
|
@computed_annotations << Archsight::Annotations::Computed.new(key, description: description, type: type, &)
|
|
58
58
|
|
|
59
59
|
# Also register as a regular annotation so it passes validation and is recognized
|
|
60
|
-
@annotations ||= []
|
|
60
|
+
@annotations ||= [] #: Array[Archsight::Annotations::Annotation]
|
|
61
61
|
options = { description: description, filter: filter, title: title, format: format, enum: enum,
|
|
62
|
-
sidebar: sidebar, type: type, list: list }
|
|
62
|
+
sidebar: sidebar, type: type, list: list, editor: editor }
|
|
63
63
|
@annotations << Archsight::Annotations::Annotation.new(key, options)
|
|
64
64
|
end
|
|
65
65
|
|
|
@@ -180,11 +180,11 @@ module Archsight
|
|
|
180
180
|
# @param key [String] The annotation key
|
|
181
181
|
# @param value [Object] The computed value
|
|
182
182
|
def set_computed_annotation(key, value)
|
|
183
|
-
@computed_values ||= {}
|
|
183
|
+
@computed_values ||= {} #: Hash[String, untyped]
|
|
184
184
|
@computed_values[key] = value
|
|
185
185
|
# Write to annotations hash for query compatibility
|
|
186
186
|
@raw["metadata"] ||= {}
|
|
187
|
-
@raw["metadata"]["annotations"]
|
|
187
|
+
@raw["metadata"]["annotations"] = @raw["metadata"]["annotations"] || {} #: Hash[String, String]
|
|
188
188
|
@raw["metadata"]["annotations"][key] = value
|
|
189
189
|
end
|
|
190
190
|
|
|
@@ -241,13 +241,13 @@ module Archsight
|
|
|
241
241
|
# Get references grouped by kind and verb for display (incoming)
|
|
242
242
|
# Returns: { "Kind" => { "verb" => [instances...] } }
|
|
243
243
|
def references_grouped
|
|
244
|
-
grouped = {}
|
|
244
|
+
grouped = {} #: Hash[String, Hash[untyped, Array[Base]]]
|
|
245
245
|
@references.each do |ref|
|
|
246
246
|
inst = ref[:instance]
|
|
247
247
|
verb = ref[:verb]
|
|
248
248
|
kind = inst.klass
|
|
249
249
|
grouped[kind] ||= {}
|
|
250
|
-
grouped[kind][verb] ||= []
|
|
250
|
+
grouped[kind][verb] ||= [] #: Array[Base]
|
|
251
251
|
grouped[kind][verb] << inst
|
|
252
252
|
end
|
|
253
253
|
# Sort by kind name, then by verb name
|
|
@@ -257,7 +257,7 @@ module Archsight
|
|
|
257
257
|
# Get outgoing relations grouped by verb and kind for display
|
|
258
258
|
# Returns: { "verb" => { "Kind" => [instances...] } }
|
|
259
259
|
def relations_grouped
|
|
260
|
-
grouped = {}
|
|
260
|
+
grouped = {} #: Hash[String, Hash[String, Array[Base]]]
|
|
261
261
|
spec.each do |verb, kinds|
|
|
262
262
|
next unless kinds.is_a?(Hash)
|
|
263
263
|
|
|
@@ -267,7 +267,7 @@ module Archsight
|
|
|
267
267
|
instances.each do |inst|
|
|
268
268
|
kind = inst.klass
|
|
269
269
|
grouped[verb] ||= {}
|
|
270
|
-
grouped[verb][kind] ||= []
|
|
270
|
+
grouped[verb][kind] ||= [] #: Array[Base]
|
|
271
271
|
grouped[verb][kind] << inst
|
|
272
272
|
end
|
|
273
273
|
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
# TechnologyInterface is the backing of an applicationInterface
|
|
4
4
|
class Archsight::Resources::TechnologyInterface < Archsight::Resources::Base
|
|
5
|
-
include_annotations :git, :architecture
|
|
5
|
+
include_annotations :git, :architecture, :interface
|
|
6
6
|
|
|
7
7
|
description <<~MD
|
|
8
8
|
Represents a point of access where technology services are made available.
|
|
@@ -30,6 +30,11 @@ class Archsight::Resources::TechnologyService < Archsight::Resources::Base
|
|
|
30
30
|
icon "cloud"
|
|
31
31
|
layer "technology"
|
|
32
32
|
|
|
33
|
+
annotation "architecture/applicationSets",
|
|
34
|
+
description: "Related ArgoCD ApplicationSets",
|
|
35
|
+
title: "ApplicationSets",
|
|
36
|
+
format: :markdown
|
|
37
|
+
|
|
33
38
|
relation :suppliedBy, :technologyComponents, :TechnologySystemSoftware
|
|
34
39
|
relation :servedBy, :businessActors, :BusinessActor
|
|
35
40
|
end
|
data/lib/archsight/version.rb
CHANGED
|
@@ -16,6 +16,7 @@ require_relative "../resources"
|
|
|
16
16
|
require_relative "../mcp"
|
|
17
17
|
require_relative "api/routes"
|
|
18
18
|
require_relative "api/docs"
|
|
19
|
+
require_relative "editor/routes"
|
|
19
20
|
|
|
20
21
|
# Define the Web namespace before the class definition
|
|
21
22
|
module Archsight::Web; end
|
|
@@ -59,6 +60,7 @@ class Archsight::Web::Application < Sinatra::Base
|
|
|
59
60
|
set :haml, format: :html5
|
|
60
61
|
set :server, :puma
|
|
61
62
|
set :reload_enabled, true
|
|
63
|
+
set :inline_edit_enabled, false
|
|
62
64
|
end
|
|
63
65
|
|
|
64
66
|
# MCP Server setup
|
|
@@ -74,6 +76,7 @@ class Archsight::Web::Application < Sinatra::Base
|
|
|
74
76
|
mcp_server.register_tool(Archsight::MCP::QueryTool)
|
|
75
77
|
mcp_server.register_tool(Archsight::MCP::AnalyzeResourceTool)
|
|
76
78
|
mcp_server.register_tool(Archsight::MCP::ResourceDocTool)
|
|
79
|
+
mcp_server.register_tool(Archsight::MCP::ExecuteAnalysisTool)
|
|
77
80
|
|
|
78
81
|
use FastMcp::Transports::RackTransport, mcp_server,
|
|
79
82
|
path_prefix: "/mcp",
|
|
@@ -85,6 +88,7 @@ class Archsight::Web::Application < Sinatra::Base
|
|
|
85
88
|
# Register API modules
|
|
86
89
|
register Archsight::Web::API::Routes
|
|
87
90
|
register Archsight::Web::API::Docs
|
|
91
|
+
register Archsight::Web::Editor::Routes
|
|
88
92
|
|
|
89
93
|
helpers do
|
|
90
94
|
def db
|
|
@@ -95,6 +99,10 @@ class Archsight::Web::Application < Sinatra::Base
|
|
|
95
99
|
settings.reload_enabled
|
|
96
100
|
end
|
|
97
101
|
|
|
102
|
+
def inline_edit_enabled?
|
|
103
|
+
settings.inline_edit_enabled
|
|
104
|
+
end
|
|
105
|
+
|
|
98
106
|
def production?
|
|
99
107
|
settings.environment == :production
|
|
100
108
|
end
|
|
@@ -104,8 +104,9 @@ Lists repositories from a GitLab instance and generates child Import resources.
|
|
|
104
104
|
- `host` - GitLab host (required)
|
|
105
105
|
- `exploreGroups` - If "true", explore all visible groups (default: false)
|
|
106
106
|
- `repoOutputPath` - Output path for repository handler results
|
|
107
|
-
- `fallbackTeam` - Default team when no contributor match found
|
|
108
|
-
- `botTeam` - Team for bot-only repositories
|
|
107
|
+
- `fallbackTeam` - Default team when no contributor match found (propagated to child imports)
|
|
108
|
+
- `botTeam` - Team for bot-only repositories (propagated to child imports)
|
|
109
|
+
- `corporateAffixes` - Comma-separated corporate username affixes for team matching, e.g., "ionos,1and1" (propagated to child imports)
|
|
109
110
|
|
|
110
111
|
**Environment:**
|
|
111
112
|
- `GITLAB_TOKEN` - Personal access token (required)
|
|
@@ -119,6 +120,9 @@ Lists repositories from a GitHub organization and generates child Import resourc
|
|
|
119
120
|
**Configuration:**
|
|
120
121
|
- `org` - GitHub organization (required)
|
|
121
122
|
- `repoOutputPath` - Output path for repository handler results
|
|
123
|
+
- `fallbackTeam` - Default team when no contributor match found (propagated to child imports)
|
|
124
|
+
- `botTeam` - Team for bot-only repositories (propagated to child imports)
|
|
125
|
+
- `corporateAffixes` - Comma-separated corporate username affixes for team matching, e.g., "ionos,1and1" (propagated to child imports)
|
|
122
126
|
|
|
123
127
|
**Environment:**
|
|
124
128
|
- `GITHUB_TOKEN` - GitHub Personal Access Token (required)
|
|
@@ -140,6 +144,7 @@ Analyzes a single git repository and generates a TechnologyArtifact resource.
|
|
|
140
144
|
- `sccPath` - Path to scc binary (default: scc)
|
|
141
145
|
- `fallbackTeam` - Default team when no contributor match found
|
|
142
146
|
- `botTeam` - Team for bot-only repositories
|
|
147
|
+
- `corporateAffixes` - Comma-separated corporate username affixes for team matching (e.g., "ionos,1and1"). Enables matching git usernames like `jsmith-ionos` or `ionos-jsmith` to team member "John Smith". Without this, corporate username pattern matching is inactive.
|
|
143
148
|
|
|
144
149
|
**Output:** Generates one TechnologyArtifact resource with:
|
|
145
150
|
- Code analysis metrics (languages, LOC, estimated cost)
|
|
@@ -287,6 +292,7 @@ metadata:
|
|
|
287
292
|
import/config/host: gitlab.company.com
|
|
288
293
|
import/config/repoOutputPath: generated/repositories.yaml
|
|
289
294
|
import/config/fallbackTeam: "Team:Platform"
|
|
295
|
+
import/config/corporateAffixes: "ionos,1and1"
|
|
290
296
|
spec: {}
|
|
291
297
|
```
|
|
292
298
|
|
|
@@ -305,6 +311,8 @@ metadata:
|
|
|
305
311
|
import/config/gitUrl: git@gitlab.company.com:company/my-service.git
|
|
306
312
|
import/config/archived: "false"
|
|
307
313
|
import/config/visibility: internal
|
|
314
|
+
import/config/fallbackTeam: "Team:Platform"
|
|
315
|
+
import/config/corporateAffixes: "ionos,1and1"
|
|
308
316
|
import/outputPath: generated/repositories.yaml
|
|
309
317
|
spec: {}
|
|
310
318
|
---
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../editor"
|
|
4
|
+
|
|
5
|
+
module Archsight
|
|
6
|
+
module Web
|
|
7
|
+
module Editor
|
|
8
|
+
# FormBuilder generates form field metadata from annotation definitions
|
|
9
|
+
class FormBuilder
|
|
10
|
+
# Field represents a single form field configuration
|
|
11
|
+
Field = Struct.new(:key, :title, :description, :input_type, :options, :step, :required, :code_language, keyword_init: true) do
|
|
12
|
+
def select?
|
|
13
|
+
input_type == :select
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def textarea?
|
|
17
|
+
%i[textarea markdown].include?(input_type)
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def markdown?
|
|
21
|
+
input_type == :markdown
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def code?
|
|
25
|
+
input_type == :code
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def number?
|
|
29
|
+
input_type == :number
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def url?
|
|
33
|
+
input_type == :url
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def text?
|
|
37
|
+
input_type == :text
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def list?
|
|
41
|
+
input_type == :list
|
|
42
|
+
end
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# Build form fields for a resource kind
|
|
46
|
+
# @param kind [String] Resource kind
|
|
47
|
+
# @return [Array<Field>]
|
|
48
|
+
def self.fields_for(kind)
|
|
49
|
+
annotations = Archsight::Editor.editable_annotations(kind)
|
|
50
|
+
|
|
51
|
+
annotations.map do |ann|
|
|
52
|
+
Field.new(
|
|
53
|
+
key: ann.key,
|
|
54
|
+
title: ann.title,
|
|
55
|
+
description: ann.description,
|
|
56
|
+
input_type: determine_input_type(ann),
|
|
57
|
+
options: ann.enum,
|
|
58
|
+
step: determine_step(ann),
|
|
59
|
+
required: false,
|
|
60
|
+
code_language: ann.code_language
|
|
61
|
+
)
|
|
62
|
+
end
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
# Determine input type based on annotation properties
|
|
66
|
+
# @param annotation [Archsight::Annotations::Annotation]
|
|
67
|
+
# @return [Symbol]
|
|
68
|
+
def self.determine_input_type(annotation)
|
|
69
|
+
return :select if annotation.enum
|
|
70
|
+
|
|
71
|
+
case annotation.type.to_s
|
|
72
|
+
when "Integer", "Float"
|
|
73
|
+
:number
|
|
74
|
+
when "URI"
|
|
75
|
+
:url
|
|
76
|
+
else
|
|
77
|
+
return :markdown if annotation.markdown?
|
|
78
|
+
return :textarea if annotation.multiline?
|
|
79
|
+
return :code if annotation.code?
|
|
80
|
+
return :list if annotation.list?
|
|
81
|
+
|
|
82
|
+
:text
|
|
83
|
+
end
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
# Determine step attribute for number inputs
|
|
87
|
+
# @param annotation [Archsight::Annotations::Annotation]
|
|
88
|
+
# @return [String, nil]
|
|
89
|
+
def self.determine_step(annotation)
|
|
90
|
+
case annotation.type.to_s
|
|
91
|
+
when "Integer"
|
|
92
|
+
"1"
|
|
93
|
+
when "Float"
|
|
94
|
+
"0.01"
|
|
95
|
+
end
|
|
96
|
+
end
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
end
|
|
100
|
+
end
|
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "sinatra/base"
|
|
4
|
+
require "sinatra/extension"
|
|
5
|
+
require_relative "../../editor"
|
|
6
|
+
require_relative "form_builder"
|
|
7
|
+
|
|
8
|
+
module Archsight
|
|
9
|
+
module Web
|
|
10
|
+
module Editor
|
|
11
|
+
# Routes for the resource editor
|
|
12
|
+
module Routes
|
|
13
|
+
extend Sinatra::Extension
|
|
14
|
+
|
|
15
|
+
helpers do
|
|
16
|
+
# Get form fields for a resource kind
|
|
17
|
+
def editor_fields(kind)
|
|
18
|
+
FormBuilder.fields_for(kind)
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Get relation verbs for a resource kind
|
|
22
|
+
def relation_verbs(kind)
|
|
23
|
+
Archsight::Editor.relation_verbs(kind)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# Get available relations for a resource kind
|
|
27
|
+
def available_relations(kind)
|
|
28
|
+
Archsight::Editor.available_relations(kind)
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Extract annotations from form params
|
|
32
|
+
def extract_annotations(params)
|
|
33
|
+
annotations = params["annotations"] || {}
|
|
34
|
+
annotations.transform_values { |v| v.is_a?(String) ? v.strip : v }
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
# Extract relations from form params
|
|
38
|
+
# Relations come in as arrays: verb[], kind[], name[]
|
|
39
|
+
def parse_form_relations(params)
|
|
40
|
+
relations = []
|
|
41
|
+
relation_data = params["relations"] || []
|
|
42
|
+
|
|
43
|
+
relation_data.each do |rel|
|
|
44
|
+
next unless rel.is_a?(Hash)
|
|
45
|
+
|
|
46
|
+
verb = rel["verb"]&.strip
|
|
47
|
+
kind = rel["kind"]&.strip
|
|
48
|
+
name = rel["name"]&.strip
|
|
49
|
+
|
|
50
|
+
next if verb.nil? || verb.empty?
|
|
51
|
+
next if kind.nil? || kind.empty?
|
|
52
|
+
next if name.nil? || name.empty?
|
|
53
|
+
|
|
54
|
+
# Find existing relation group or create new one
|
|
55
|
+
existing = relations.find { |r| r[:verb] == verb && r[:kind] == kind }
|
|
56
|
+
if existing
|
|
57
|
+
existing[:names] << name unless existing[:names].include?(name)
|
|
58
|
+
else
|
|
59
|
+
relations << { verb: verb, kind: kind, names: [name] }
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
relations
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
# Validate content hash for optimistic locking
|
|
67
|
+
def validate_content_hash(instance, expected_hash)
|
|
68
|
+
Archsight::Editor::ContentHasher.validate(
|
|
69
|
+
path: instance.path_ref.path,
|
|
70
|
+
start_line: instance.path_ref.line_no,
|
|
71
|
+
expected_hash: expected_hash
|
|
72
|
+
)
|
|
73
|
+
end
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
# Create mode - empty form
|
|
77
|
+
get "/kinds/:kind/new" do
|
|
78
|
+
@kind = params["kind"]
|
|
79
|
+
@klass = Archsight::Resources[@kind]
|
|
80
|
+
halt 404, "Kind not found" unless @klass
|
|
81
|
+
|
|
82
|
+
@editor_mode = true
|
|
83
|
+
@mode = :create
|
|
84
|
+
@name = ""
|
|
85
|
+
@annotations = {}
|
|
86
|
+
@relations = []
|
|
87
|
+
@fields = editor_fields(@kind)
|
|
88
|
+
@errors = {}
|
|
89
|
+
|
|
90
|
+
haml :index
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
# Create mode - generate YAML
|
|
94
|
+
post "/kinds/:kind/generate" do
|
|
95
|
+
@kind = params["kind"]
|
|
96
|
+
@klass = Archsight::Resources[@kind]
|
|
97
|
+
halt 404, "Kind not found" unless @klass
|
|
98
|
+
|
|
99
|
+
@editor_mode = true
|
|
100
|
+
@mode = :create
|
|
101
|
+
@name = (params["name"] || "").strip
|
|
102
|
+
@annotations = extract_annotations(params)
|
|
103
|
+
@relations = parse_form_relations(params)
|
|
104
|
+
@fields = editor_fields(@kind)
|
|
105
|
+
|
|
106
|
+
# Validate
|
|
107
|
+
validation = Archsight::Editor.validate(@kind, name: @name, annotations: @annotations)
|
|
108
|
+
|
|
109
|
+
unless validation[:valid]
|
|
110
|
+
@errors = validation[:errors]
|
|
111
|
+
return haml :index
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
# Build and render YAML
|
|
115
|
+
resource = Archsight::Editor.build_resource(
|
|
116
|
+
kind: @kind,
|
|
117
|
+
name: @name,
|
|
118
|
+
annotations: @annotations,
|
|
119
|
+
relations: @relations
|
|
120
|
+
)
|
|
121
|
+
|
|
122
|
+
@generated_yaml = Archsight::Editor.to_yaml(resource)
|
|
123
|
+
@errors = {}
|
|
124
|
+
|
|
125
|
+
haml :index
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Edit mode - pre-filled form
|
|
129
|
+
get "/kinds/:kind/instances/:name/edit" do
|
|
130
|
+
@kind = params["kind"]
|
|
131
|
+
@instance_name = params["name"]
|
|
132
|
+
@klass = Archsight::Resources[@kind]
|
|
133
|
+
halt 404, "Kind not found" unless @klass
|
|
134
|
+
|
|
135
|
+
instance = db.instance_by_kind(@kind, @instance_name)
|
|
136
|
+
halt 404, "Instance not found" unless instance
|
|
137
|
+
|
|
138
|
+
@editor_mode = true
|
|
139
|
+
@mode = :edit
|
|
140
|
+
@name = instance.name
|
|
141
|
+
@annotations = instance.annotations.dup
|
|
142
|
+
@relations = extract_instance_relations(instance)
|
|
143
|
+
@fields = editor_fields(@kind)
|
|
144
|
+
@errors = {}
|
|
145
|
+
|
|
146
|
+
# Compute content hash for optimistic locking
|
|
147
|
+
original_content = Archsight::Editor::FileWriter.read_document(
|
|
148
|
+
path: instance.path_ref.path,
|
|
149
|
+
start_line: instance.path_ref.line_no
|
|
150
|
+
)
|
|
151
|
+
@content_hash = Archsight::Editor::ContentHasher.hash(original_content)
|
|
152
|
+
|
|
153
|
+
haml :index
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# Edit mode - generate YAML
|
|
157
|
+
post "/kinds/:kind/instances/:name/generate" do
|
|
158
|
+
@kind = params["kind"]
|
|
159
|
+
@instance_name = params["name"]
|
|
160
|
+
@klass = Archsight::Resources[@kind]
|
|
161
|
+
halt 404, "Kind not found" unless @klass
|
|
162
|
+
|
|
163
|
+
# Get original instance for path_ref (for inline save)
|
|
164
|
+
original_instance = db.instance_by_kind(@kind, @instance_name)
|
|
165
|
+
@path_ref = original_instance&.path_ref
|
|
166
|
+
|
|
167
|
+
@editor_mode = true
|
|
168
|
+
@mode = :edit
|
|
169
|
+
@name = (params["name_field"] || @instance_name).strip
|
|
170
|
+
@annotations = extract_annotations(params)
|
|
171
|
+
@relations = parse_form_relations(params)
|
|
172
|
+
@fields = editor_fields(@kind)
|
|
173
|
+
@content_hash = params["content_hash"]
|
|
174
|
+
|
|
175
|
+
# Validate
|
|
176
|
+
validation = Archsight::Editor.validate(@kind, name: @name, annotations: @annotations)
|
|
177
|
+
|
|
178
|
+
unless validation[:valid]
|
|
179
|
+
@errors = validation[:errors]
|
|
180
|
+
return haml :index
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
# Build and render YAML
|
|
184
|
+
resource = Archsight::Editor.build_resource(
|
|
185
|
+
kind: @kind,
|
|
186
|
+
name: @name,
|
|
187
|
+
annotations: @annotations,
|
|
188
|
+
relations: @relations
|
|
189
|
+
)
|
|
190
|
+
|
|
191
|
+
@generated_yaml = Archsight::Editor.to_yaml(resource)
|
|
192
|
+
@errors = {}
|
|
193
|
+
|
|
194
|
+
haml :index
|
|
195
|
+
end
|
|
196
|
+
|
|
197
|
+
# Save YAML to source file (inline edit)
|
|
198
|
+
post "/api/v1/editor/kinds/:kind/instances/:name/save" do
|
|
199
|
+
content_type :json
|
|
200
|
+
|
|
201
|
+
# Check if inline edit is enabled
|
|
202
|
+
halt 403, JSON.generate({ success: false, error: "Inline edit is disabled. Start server with --inline-edit flag." }) unless settings.inline_edit_enabled
|
|
203
|
+
|
|
204
|
+
kind = params["kind"]
|
|
205
|
+
name = params["name"]
|
|
206
|
+
|
|
207
|
+
begin
|
|
208
|
+
body = JSON.parse(request.body.read)
|
|
209
|
+
yaml_content = body["yaml"]
|
|
210
|
+
expected_hash = body["content_hash"]
|
|
211
|
+
rescue JSON::ParserError
|
|
212
|
+
halt 400, JSON.generate({ success: false, error: "Invalid JSON" })
|
|
213
|
+
end
|
|
214
|
+
|
|
215
|
+
instance = db.instance_by_kind(kind, name)
|
|
216
|
+
halt 404, JSON.generate({ success: false, error: "Instance not found" }) unless instance
|
|
217
|
+
|
|
218
|
+
# Optimistic locking: verify content hasn't changed since edit started
|
|
219
|
+
if (conflict = validate_content_hash(instance, expected_hash))
|
|
220
|
+
status 409
|
|
221
|
+
return JSON.generate({ success: false }.merge(conflict))
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
begin
|
|
225
|
+
Archsight::Editor::FileWriter.replace_document(
|
|
226
|
+
path: instance.path_ref.path,
|
|
227
|
+
start_line: instance.path_ref.line_no,
|
|
228
|
+
new_yaml: yaml_content
|
|
229
|
+
)
|
|
230
|
+
db.reload!
|
|
231
|
+
JSON.generate({ success: true, message: "Saved to #{instance.path_ref}" })
|
|
232
|
+
rescue Archsight::Editor::FileWriter::WriteError => e
|
|
233
|
+
status 400
|
|
234
|
+
JSON.generate({ success: false, error: e.message })
|
|
235
|
+
end
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
# HTMX API - Get instance names for a kind (for relation dropdown)
|
|
239
|
+
get "/api/v1/editor/kinds/:kind/instances" do
|
|
240
|
+
kind = params["kind"]
|
|
241
|
+
klass = Archsight::Resources[kind]
|
|
242
|
+
halt 404, "Kind not found" unless klass
|
|
243
|
+
|
|
244
|
+
instances = db.instances_by_kind(kind).keys.sort
|
|
245
|
+
|
|
246
|
+
content_type :json
|
|
247
|
+
JSON.generate(instances)
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
# HTMX API - Get valid target kinds for a verb
|
|
251
|
+
get "/api/v1/editor/relation-kinds" do
|
|
252
|
+
kind = params["kind"]
|
|
253
|
+
verb = params["verb"]
|
|
254
|
+
halt 400, "Kind and verb required" unless kind && verb
|
|
255
|
+
|
|
256
|
+
target_kinds = Archsight::Editor.target_kinds_for_verb(kind, verb)
|
|
257
|
+
|
|
258
|
+
content_type :json
|
|
259
|
+
JSON.generate(target_kinds)
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
helpers do
|
|
263
|
+
# Extract relations from an existing instance into form format
|
|
264
|
+
# Returns relations with target class names (e.g., "BusinessActor")
|
|
265
|
+
# not relation names (e.g., "businessActors")
|
|
266
|
+
def extract_instance_relations(instance)
|
|
267
|
+
relations = []
|
|
268
|
+
|
|
269
|
+
instance.spec.each do |verb, relation_groups|
|
|
270
|
+
next unless relation_groups.is_a?(Hash)
|
|
271
|
+
|
|
272
|
+
relation_groups.each do |relation_name, targets|
|
|
273
|
+
next unless targets.is_a?(Array)
|
|
274
|
+
|
|
275
|
+
# Look up the target class name from the relation definition
|
|
276
|
+
target_class = Archsight::Editor.target_class_for_relation(instance.kind, verb, relation_name)
|
|
277
|
+
next unless target_class
|
|
278
|
+
|
|
279
|
+
targets.each do |target|
|
|
280
|
+
# Target could be an instance object or a string
|
|
281
|
+
target_name = target.respond_to?(:name) ? target.name : target.to_s
|
|
282
|
+
relations << { verb: verb, kind: target_class, name: target_name }
|
|
283
|
+
end
|
|
284
|
+
end
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
relations
|
|
288
|
+
end
|
|
289
|
+
end
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
end
|