Skip to content

Commit db2220d

Browse files
Feature/editor (#26)
* feat: Implement editor functionality for resource creation and management - Add JavaScript for handling relations in the editor, including adding, removing, and sorting relations. - Create HAML partials for rendering fields, forms, relations, and YAML output in the editor. - Implement backend logic for generating YAML from resource definitions, including validation and handling of annotations and relations. - Add tests for editor functionality, including form rendering, validation, and YAML generation. - Update layout to include editor styles and scripts. - Ensure proper handling of instance details and relations in the edit mode. * feat: Add status annotation with lifecycle options to architecture annotations * feat: Add interface annotations for resource classes and update related resources * feat: Add applicationSets annotation to TechnologyService for ArgoCD integration * feat: Remove authenticationProvider annotation from ApplicationInterface * feat: Implement markdown editor with Lexical integration and UI enhancements * feat: Enhance annotation validation and add code support in editor * feat: Refactor analysis filtering and remove enabled flag from analysis resources
1 parent d0c6e63 commit db2220d

33 files changed

+3143
-130
lines changed

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,33 @@ claude mcp add --transport sse ionos-architecture http://localhost:4567/mcp/sse
129129
- Dark mode support
130130
- Layer-based color scheme (Business, Application, Technology, Data)
131131

132+
### Resource Editor
133+
134+
Create and edit resources through the web interface:
135+
136+
**Edit existing resource:**
137+
138+
- Navigate to any resource detail page
139+
- Click the "Edit" button (only available for non-generated resources)
140+
- Modify annotations and relations
141+
- Generate YAML and copy to clipboard
142+
143+
**Create new resource:**
144+
145+
- Go to any kind listing (e.g., /kinds/ApplicationComponent)
146+
- Click "New" button
147+
- Fill in required fields
148+
- Add relations using cascading dropdowns
149+
- Generate YAML and copy to clipboard
150+
151+
The editor supports:
152+
153+
- Type-aware form fields (dropdowns for enums, number inputs, URL validation)
154+
- Markdown textarea for descriptions
155+
- Relation management with cascading dropdowns
156+
- Validation before YAML generation
157+
- One-click copy to clipboard
158+
132159
### Validation
133160

134161
Validate YAML syntax and verify all relationship references:

examples/archsight/generated/archsight-interface.yaml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ metadata:
5353
- `-> none & <- none` - true orphans with no relations at all
5454
architecture/openapi: 3.1.0
5555
architecture/version: '1.0'
56-
architecture/status: GA
56+
architecture/status: General-Availability
5757
architecture/visibility: public
5858
architecture/tags: https,rest
5959
architecture/encoding: json

examples/archsight/interfaces/interfaces.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ metadata:
1919
| `import` | Run import handlers to generate resources |
2020
| `version` | Display the archsight version |
2121
architecture/version: '1.0'
22-
architecture/status: GA
22+
architecture/status: General-Availability
2323
architecture/visibility: public
2424
architecture/tags: cli,thor,terminal
2525
architecture/documentation: https://github.com/ionos-cloud/archsight
@@ -79,7 +79,7 @@ metadata:
7979
http://localhost:6789/sse
8080
```
8181
architecture/version: '1.0'
82-
architecture/status: GA
82+
architecture/status: General-Availability
8383
architecture/visibility: public
8484
architecture/tags: mcp,ai,claude,sse,http
8585
architecture/documentation: https://modelcontextprotocol.io

lib/archsight/analysis/executor.rb

Lines changed: 0 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -79,9 +79,6 @@ def execute_all(filter: nil)
7979
# Filter by name pattern if provided
8080
analyses = analyses.select { |a| filter.match?(a.name) } if filter
8181

82-
# Filter to enabled analyses
83-
analyses = analyses.select { |a| analysis_enabled?(a) }
84-
8582
analyses.map { |analysis| execute(analysis) }
8683
end
8784

@@ -100,13 +97,6 @@ def parse_timeout(timeout_str)
10097

10198
DEFAULT_TIMEOUT
10299
end
103-
104-
# Check if analysis is enabled
105-
# @param analysis [Object] Analysis instance
106-
# @return [Boolean] true if enabled
107-
def analysis_enabled?(analysis)
108-
analysis.annotations["analysis/enabled"] != "false"
109-
end
110100
end
111101
end
112102
end

lib/archsight/annotations/annotation.rb

Lines changed: 78 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -83,40 +83,9 @@ def validate(value)
8383
errors = []
8484
return errors if value.nil?
8585

86-
# Check enum constraint
87-
if @enum
88-
values = list? ? value.to_s.split(",").map(&:strip) : [value.to_s]
89-
invalid_values = values.reject { |v| @enum.include?(v) }
90-
invalid_values.each do |v|
91-
errors << "invalid value '#{v}'. Expected one of: #{@enum.join(", ")}"
92-
end
93-
end
94-
95-
# Check type constraint
96-
if @type.is_a?(Class) && errors.empty?
97-
values_to_check = list? ? value.to_s.split(/,|\n/).map(&:strip).reject(&:empty?) : [value.to_s]
98-
99-
values_to_check.each do |string_value|
100-
valid = case @type.to_s
101-
when "Integer"
102-
string_value.match?(/\A-?\d+\z/)
103-
when "Float"
104-
string_value.match?(/\A-?\d+(\.\d+)?\z/)
105-
when "URI"
106-
begin
107-
URI.parse(string_value)
108-
string_value.match?(%r{\Ahttps?://})
109-
rescue URI::InvalidURIError
110-
false
111-
end
112-
when "Archsight::Annotations::EmailRecipient"
113-
Archsight::Annotations::EmailRecipient.valid?(string_value)
114-
else
115-
true
116-
end
117-
errors << "invalid value '#{string_value}'. #{type_error_message}" unless valid
118-
end
119-
end
86+
validate_enum(value, errors)
87+
validate_type(value, errors) if errors.empty?
88+
validate_code(value, errors) if errors.empty?
12089

12190
errors
12291
end
@@ -130,6 +99,14 @@ def markdown?
13099
@format == :markdown
131100
end
132101

102+
def code?
103+
@format == :ruby
104+
end
105+
106+
def code_language
107+
@format if code?
108+
end
109+
133110
# Example value for templates
134111
def example_value
135112
if @enum
@@ -155,6 +132,49 @@ def type_error_message
155132
end
156133
end
157134

135+
def validate_enum(value, errors)
136+
return unless @enum
137+
138+
values = list? ? value.to_s.split(",").map(&:strip) : [value.to_s]
139+
invalid_values = values.reject { |v| @enum.include?(v) }
140+
invalid_values.each do |v|
141+
errors << "invalid value '#{v}'. Expected one of: #{@enum.join(", ")}"
142+
end
143+
end
144+
145+
def validate_type(value, errors)
146+
return unless @type.is_a?(Class)
147+
148+
values_to_check = list? ? value.to_s.split(/,|\n/).map(&:strip).reject(&:empty?) : [value.to_s]
149+
values_to_check.each do |string_value|
150+
errors << "invalid value '#{string_value}'. #{type_error_message}" unless valid_type_value?(string_value)
151+
end
152+
end
153+
154+
def valid_type_value?(string_value)
155+
case @type.to_s
156+
when "Integer" then string_value.match?(/\A-?\d+\z/)
157+
when "Float" then string_value.match?(/\A-?\d+(\.\d+)?\z/)
158+
when "URI" then valid_uri?(string_value)
159+
when "Archsight::Annotations::EmailRecipient" then Archsight::Annotations::EmailRecipient.valid?(string_value)
160+
else true
161+
end
162+
end
163+
164+
def valid_uri?(string_value)
165+
URI.parse(string_value)
166+
string_value.match?(%r{\Ahttps?://})
167+
rescue URI::InvalidURIError
168+
false
169+
end
170+
171+
def validate_code(value, errors)
172+
return unless code? && !value.to_s.strip.empty?
173+
174+
syntax_error = validate_code_syntax(value.to_s)
175+
errors << syntax_error if syntax_error
176+
end
177+
158178
def derive_format
159179
case @filter
160180
when :word then :tag_word
@@ -165,4 +185,28 @@ def derive_format
165185
def build_regex
166186
Regexp.new("^#{Regexp.escape(key).gsub('\*', ".+")}$")
167187
end
188+
189+
# Validate code syntax based on format
190+
# @param code [String] The code to validate
191+
# @return [String, nil] Error message or nil if valid
192+
def validate_code_syntax(code)
193+
case @format
194+
when :ruby
195+
validate_ruby_syntax(code)
196+
end
197+
end
198+
199+
# Validate Ruby syntax using RubyVM
200+
# @param code [String] Ruby code to validate
201+
# @return [String, nil] Error message or nil if valid
202+
def validate_ruby_syntax(code)
203+
# steep:ignore:start
204+
RubyVM::InstructionSequence.compile(code)
205+
# steep:ignore:end
206+
nil
207+
rescue SyntaxError => e
208+
# Extract just the error message without the full backtrace
209+
message = e.message.lines.first&.strip || "Syntax error"
210+
"Ruby syntax error: #{message}"
211+
end
168212
end

lib/archsight/annotations/architecture_annotations.rb

Lines changed: 1 addition & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,8 @@ def self.included(base)
99
annotation "architecture/abbr",
1010
description: "Abbreviation or short name",
1111
title: "Abbreviation"
12-
annotation "architecture/evidence",
13-
description: "Supporting evidence or notes",
14-
title: "Evidence",
15-
format: :markdown
1612
annotation "architecture/description",
17-
description: "Textual description of the interface",
13+
description: "Textual description of the resource",
1814
title: "Description",
1915
format: :markdown
2016
annotation "architecture/documentation",
@@ -25,35 +21,6 @@ def self.included(base)
2521
description: "Comma-separated tags",
2622
filter: :list,
2723
title: "Tags"
28-
annotation "architecture/encoding",
29-
description: "Data encoding format",
30-
filter: :list,
31-
title: "Encoding"
32-
annotation "architecture/title",
33-
description: "Interface title",
34-
title: "Title"
35-
annotation "architecture/openapi",
36-
description: "OpenAPI specification version",
37-
filter: :word,
38-
title: "OpenAPI"
39-
annotation "architecture/version",
40-
description: "API or interface version",
41-
filter: :word,
42-
title: "Version",
43-
sidebar: false
44-
annotation "architecture/status",
45-
description: "Lifecycle status (General-Availability, Early-Access, Development)",
46-
filter: :word,
47-
title: "Status"
48-
annotation "architecture/visibility",
49-
description: "API visibility (public, private, internal)",
50-
filter: :word,
51-
enum: %w[public private internal],
52-
title: "Visibility"
53-
annotation "architecture/applicationSets",
54-
description: "Related ArgoCD ApplicationSets",
55-
title: "ApplicationSets",
56-
format: :markdown
5724
end
5825
end
5926
end
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
# frozen_string_literal: true
2+
3+
# Interface module adds interface-specific annotations to resource classes
4+
module Archsight::Annotations::Interface
5+
def self.included(base)
6+
base.class_eval do
7+
annotation "architecture/encoding",
8+
description: "Data encoding format",
9+
filter: :list,
10+
title: "Encoding"
11+
annotation "architecture/title",
12+
description: "Interface title",
13+
title: "Title"
14+
annotation "architecture/openapi",
15+
description: "OpenAPI specification version",
16+
filter: :word,
17+
title: "OpenAPI"
18+
annotation "architecture/version",
19+
description: "API or interface version",
20+
filter: :word,
21+
title: "Version",
22+
sidebar: false
23+
annotation "architecture/status",
24+
description: "Lifecycle status",
25+
filter: :word,
26+
enum: %w[General-Availability Early-Access Development],
27+
title: "Status"
28+
annotation "architecture/visibility",
29+
description: "API visibility (public, private, internal)",
30+
filter: :word,
31+
enum: %w[public private internal],
32+
title: "Visibility"
33+
end
34+
end
35+
end

lib/archsight/cli.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -191,7 +191,7 @@ def load_database_for_analysis
191191
def filter_analyses(db)
192192
analyses = db.instances_by_kind("Analysis").values
193193
analyses = analyses.select { |a| Regexp.new(options[:filter], Regexp::IGNORECASE).match?(a.name) } if options[:filter]
194-
analyses.reject { |a| a.annotations["analysis/enabled"] == "false" }
194+
analyses
195195
end
196196

197197
def print_analysis_dry_run(analyses)

0 commit comments

Comments
 (0)