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
|
@@ -0,0 +1,308 @@
|
|
|
1
|
+
// Lexical Rich Text Editor Module for Markdown editing
|
|
2
|
+
// Uses ESM imports from esm.sh CDN
|
|
3
|
+
|
|
4
|
+
import { createEditor, $getRoot, $getSelection, $isRangeSelection, FORMAT_TEXT_COMMAND, $createTextNode } from 'https://esm.sh/lexical@0.20.0';
|
|
5
|
+
import { registerRichText, HeadingNode, QuoteNode, $createHeadingNode, $createQuoteNode } from 'https://esm.sh/@lexical/rich-text@0.20.0';
|
|
6
|
+
import { registerHistory, createEmptyHistoryState } from 'https://esm.sh/@lexical/history@0.20.0';
|
|
7
|
+
import { $convertFromMarkdownString, $convertToMarkdownString, TRANSFORMERS } from 'https://esm.sh/@lexical/markdown@0.20.0';
|
|
8
|
+
import { ListNode, ListItemNode, registerList, INSERT_ORDERED_LIST_COMMAND, INSERT_UNORDERED_LIST_COMMAND, REMOVE_LIST_COMMAND } from 'https://esm.sh/@lexical/list@0.20.0';
|
|
9
|
+
import { CodeNode, CodeHighlightNode, registerCodeHighlighting, $createCodeNode, $isCodeNode, getCodeLanguages } from 'https://esm.sh/@lexical/code@0.20.0';
|
|
10
|
+
import { LinkNode, $createLinkNode, $isLinkNode } from 'https://esm.sh/@lexical/link@0.20.0';
|
|
11
|
+
import { $setBlocksType } from 'https://esm.sh/@lexical/selection@0.20.0';
|
|
12
|
+
import { $createParagraphNode } from 'https://esm.sh/lexical@0.20.0';
|
|
13
|
+
|
|
14
|
+
let lexicalEditor = null;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Initialize the Lexical editor in the given container element
|
|
18
|
+
* @param {HTMLElement} containerElement - The contenteditable element to use as editor root
|
|
19
|
+
* @returns {LexicalEditor} The initialized editor instance
|
|
20
|
+
*/
|
|
21
|
+
export function initLexicalEditor(containerElement) {
|
|
22
|
+
const config = {
|
|
23
|
+
namespace: 'ArchsightMarkdownEditor',
|
|
24
|
+
nodes: [HeadingNode, QuoteNode, ListNode, ListItemNode, CodeNode, CodeHighlightNode, LinkNode],
|
|
25
|
+
onError: (error) => {
|
|
26
|
+
console.error('Lexical Editor Error:', error);
|
|
27
|
+
},
|
|
28
|
+
theme: {
|
|
29
|
+
paragraph: 'lexical-paragraph',
|
|
30
|
+
heading: {
|
|
31
|
+
h1: 'lexical-h1',
|
|
32
|
+
h2: 'lexical-h2',
|
|
33
|
+
h3: 'lexical-h3',
|
|
34
|
+
},
|
|
35
|
+
list: {
|
|
36
|
+
ul: 'lexical-ul',
|
|
37
|
+
ol: 'lexical-ol',
|
|
38
|
+
listitem: 'lexical-li',
|
|
39
|
+
},
|
|
40
|
+
quote: 'lexical-quote',
|
|
41
|
+
code: 'lexical-code',
|
|
42
|
+
codeHighlight: {
|
|
43
|
+
atrule: 'tokenAttr',
|
|
44
|
+
attr: 'tokenAttr',
|
|
45
|
+
boolean: 'tokenProperty',
|
|
46
|
+
builtin: 'tokenSelector',
|
|
47
|
+
cdata: 'tokenComment',
|
|
48
|
+
char: 'tokenSelector',
|
|
49
|
+
class: 'tokenFunction',
|
|
50
|
+
'class-name': 'tokenFunction',
|
|
51
|
+
comment: 'tokenComment',
|
|
52
|
+
constant: 'tokenProperty',
|
|
53
|
+
deleted: 'tokenProperty',
|
|
54
|
+
doctype: 'tokenComment',
|
|
55
|
+
entity: 'tokenOperator',
|
|
56
|
+
function: 'tokenFunction',
|
|
57
|
+
important: 'tokenVariable',
|
|
58
|
+
inserted: 'tokenSelector',
|
|
59
|
+
keyword: 'tokenAttr',
|
|
60
|
+
namespace: 'tokenVariable',
|
|
61
|
+
number: 'tokenProperty',
|
|
62
|
+
operator: 'tokenOperator',
|
|
63
|
+
prolog: 'tokenComment',
|
|
64
|
+
property: 'tokenProperty',
|
|
65
|
+
punctuation: 'tokenPunctuation',
|
|
66
|
+
regex: 'tokenVariable',
|
|
67
|
+
selector: 'tokenSelector',
|
|
68
|
+
string: 'tokenSelector',
|
|
69
|
+
symbol: 'tokenProperty',
|
|
70
|
+
tag: 'tokenProperty',
|
|
71
|
+
url: 'tokenOperator',
|
|
72
|
+
variable: 'tokenVariable',
|
|
73
|
+
},
|
|
74
|
+
link: 'lexical-link',
|
|
75
|
+
text: {
|
|
76
|
+
bold: 'lexical-bold',
|
|
77
|
+
italic: 'lexical-italic',
|
|
78
|
+
underline: 'lexical-underline',
|
|
79
|
+
strikethrough: 'lexical-strikethrough',
|
|
80
|
+
code: 'lexical-text-code',
|
|
81
|
+
},
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
|
|
85
|
+
lexicalEditor = createEditor(config);
|
|
86
|
+
lexicalEditor.setRootElement(containerElement);
|
|
87
|
+
|
|
88
|
+
// Register plugins
|
|
89
|
+
registerRichText(lexicalEditor);
|
|
90
|
+
registerList(lexicalEditor);
|
|
91
|
+
registerCodeHighlighting(lexicalEditor);
|
|
92
|
+
registerHistory(lexicalEditor, createEmptyHistoryState(), 300);
|
|
93
|
+
|
|
94
|
+
return lexicalEditor;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Set the editor content from markdown string
|
|
99
|
+
* @param {string} markdown - The markdown string to load
|
|
100
|
+
*/
|
|
101
|
+
export function setLexicalMarkdown(markdown) {
|
|
102
|
+
if (!lexicalEditor) return;
|
|
103
|
+
|
|
104
|
+
lexicalEditor.update(() => {
|
|
105
|
+
// Clear existing content and convert markdown
|
|
106
|
+
const root = $getRoot();
|
|
107
|
+
root.clear();
|
|
108
|
+
$convertFromMarkdownString(markdown || '', TRANSFORMERS);
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Get the current editor content as markdown string
|
|
114
|
+
* @returns {Promise<string>} The markdown string
|
|
115
|
+
*/
|
|
116
|
+
export function getLexicalMarkdown() {
|
|
117
|
+
return new Promise((resolve) => {
|
|
118
|
+
if (!lexicalEditor) {
|
|
119
|
+
resolve('');
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
lexicalEditor.update(() => {
|
|
124
|
+
const markdown = $convertToMarkdownString(TRANSFORMERS);
|
|
125
|
+
resolve(markdown);
|
|
126
|
+
});
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Get the editor instance
|
|
132
|
+
* @returns {LexicalEditor|null} The editor instance
|
|
133
|
+
*/
|
|
134
|
+
export function getEditor() {
|
|
135
|
+
return lexicalEditor;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Execute a text format command (bold, italic, etc.)
|
|
140
|
+
* @param {string} format - The format type ('bold', 'italic', 'underline', 'strikethrough', 'code')
|
|
141
|
+
*/
|
|
142
|
+
export function formatText(format) {
|
|
143
|
+
if (!lexicalEditor) return;
|
|
144
|
+
lexicalEditor.dispatchCommand(FORMAT_TEXT_COMMAND, format);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Format selected block as heading, list, quote, or paragraph
|
|
149
|
+
* @param {string} blockType - The block type ('h1', 'h2', 'h3', 'bullet', 'number', 'quote', 'paragraph')
|
|
150
|
+
*/
|
|
151
|
+
export function formatBlock(blockType) {
|
|
152
|
+
if (!lexicalEditor) return;
|
|
153
|
+
|
|
154
|
+
if (blockType === 'bullet') {
|
|
155
|
+
lexicalEditor.dispatchCommand(INSERT_UNORDERED_LIST_COMMAND, undefined);
|
|
156
|
+
return;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
if (blockType === 'number') {
|
|
160
|
+
lexicalEditor.dispatchCommand(INSERT_ORDERED_LIST_COMMAND, undefined);
|
|
161
|
+
return;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
lexicalEditor.update(() => {
|
|
165
|
+
const selection = $getSelection();
|
|
166
|
+
if ($isRangeSelection(selection)) {
|
|
167
|
+
if (blockType === 'h1' || blockType === 'h2' || blockType === 'h3') {
|
|
168
|
+
$setBlocksType(selection, () => $createHeadingNode(blockType));
|
|
169
|
+
} else if (blockType === 'quote') {
|
|
170
|
+
$setBlocksType(selection, () => $createQuoteNode());
|
|
171
|
+
} else if (blockType === 'paragraph') {
|
|
172
|
+
$setBlocksType(selection, () => $createParagraphNode());
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/**
|
|
179
|
+
* Focus the editor
|
|
180
|
+
*/
|
|
181
|
+
export function focusEditor() {
|
|
182
|
+
if (!lexicalEditor) return;
|
|
183
|
+
lexicalEditor.focus();
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Insert a code block at current selection
|
|
188
|
+
* @param {string} language - The programming language for the code block
|
|
189
|
+
*/
|
|
190
|
+
export function insertCodeBlock(language = '') {
|
|
191
|
+
if (!lexicalEditor) return;
|
|
192
|
+
|
|
193
|
+
lexicalEditor.update(() => {
|
|
194
|
+
const selection = $getSelection();
|
|
195
|
+
if ($isRangeSelection(selection)) {
|
|
196
|
+
const codeNode = $createCodeNode(language);
|
|
197
|
+
selection.insertNodes([codeNode]);
|
|
198
|
+
codeNode.select();
|
|
199
|
+
}
|
|
200
|
+
});
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Set the language for the currently selected code block
|
|
206
|
+
* @param {string} language - The programming language
|
|
207
|
+
* @returns {boolean} True if language was set, false otherwise
|
|
208
|
+
*/
|
|
209
|
+
export function setCodeBlockLanguage(language) {
|
|
210
|
+
if (!lexicalEditor) return false;
|
|
211
|
+
|
|
212
|
+
let success = false;
|
|
213
|
+
lexicalEditor.update(() => {
|
|
214
|
+
const selection = $getSelection();
|
|
215
|
+
if ($isRangeSelection(selection)) {
|
|
216
|
+
const nodes = selection.getNodes();
|
|
217
|
+
for (const node of nodes) {
|
|
218
|
+
const parent = node.getParent();
|
|
219
|
+
if ($isCodeNode(parent)) {
|
|
220
|
+
parent.setLanguage(language);
|
|
221
|
+
success = true;
|
|
222
|
+
break;
|
|
223
|
+
}
|
|
224
|
+
if ($isCodeNode(node)) {
|
|
225
|
+
node.setLanguage(language);
|
|
226
|
+
success = true;
|
|
227
|
+
break;
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
});
|
|
232
|
+
return success;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Get the current code node's language if cursor is in a code block
|
|
237
|
+
* @returns {string|null} The language or null if not in a code block
|
|
238
|
+
*/
|
|
239
|
+
export function getCurrentCodeLanguage() {
|
|
240
|
+
if (!lexicalEditor) return null;
|
|
241
|
+
|
|
242
|
+
let language = null;
|
|
243
|
+
lexicalEditor.getEditorState().read(() => {
|
|
244
|
+
const selection = $getSelection();
|
|
245
|
+
if ($isRangeSelection(selection)) {
|
|
246
|
+
const nodes = selection.getNodes();
|
|
247
|
+
for (const node of nodes) {
|
|
248
|
+
const parent = node.getParent();
|
|
249
|
+
if ($isCodeNode(parent)) {
|
|
250
|
+
language = parent.getLanguage();
|
|
251
|
+
break;
|
|
252
|
+
}
|
|
253
|
+
if ($isCodeNode(node)) {
|
|
254
|
+
language = node.getLanguage();
|
|
255
|
+
break;
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
});
|
|
260
|
+
return language;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Insert or toggle a link
|
|
265
|
+
* Prompts user for URL if creating a new link
|
|
266
|
+
*/
|
|
267
|
+
export function insertLink() {
|
|
268
|
+
if (!lexicalEditor) return;
|
|
269
|
+
|
|
270
|
+
const url = prompt('Enter URL:', 'https://');
|
|
271
|
+
if (url === null) return; // User cancelled
|
|
272
|
+
|
|
273
|
+
lexicalEditor.update(() => {
|
|
274
|
+
const selection = $getSelection();
|
|
275
|
+
if (!$isRangeSelection(selection)) return;
|
|
276
|
+
|
|
277
|
+
if (url === '') {
|
|
278
|
+
// Remove link - unwrap link nodes
|
|
279
|
+
const nodes = selection.getNodes();
|
|
280
|
+
nodes.forEach(node => {
|
|
281
|
+
const parent = node.getParent();
|
|
282
|
+
if ($isLinkNode(parent)) {
|
|
283
|
+
const children = parent.getChildren();
|
|
284
|
+
for (const child of children) {
|
|
285
|
+
parent.insertBefore(child);
|
|
286
|
+
}
|
|
287
|
+
parent.remove();
|
|
288
|
+
}
|
|
289
|
+
});
|
|
290
|
+
} else {
|
|
291
|
+
// Create link by wrapping selected content
|
|
292
|
+
const selectedText = selection.getTextContent();
|
|
293
|
+
if (selectedText) {
|
|
294
|
+
// Has selection - wrap it in a link
|
|
295
|
+
const linkNode = $createLinkNode(url);
|
|
296
|
+
selection.insertNodes([linkNode]);
|
|
297
|
+
// Move selected text into link
|
|
298
|
+
linkNode.append($createTextNode(selectedText));
|
|
299
|
+
} else {
|
|
300
|
+
// No selection - insert link with URL as text
|
|
301
|
+
const linkNode = $createLinkNode(url);
|
|
302
|
+
linkNode.append($createTextNode(url));
|
|
303
|
+
selection.insertNodes([linkNode]);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
});
|
|
307
|
+
}
|
|
308
|
+
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
.field-group{class: field.textarea? || field.code? ? "field-full-width" : ""}
|
|
2
|
+
.label-row
|
|
3
|
+
%label{for: field.key, title: field.description}
|
|
4
|
+
= field.title
|
|
5
|
+
- if field.required
|
|
6
|
+
%span.required *
|
|
7
|
+
- if field.markdown?
|
|
8
|
+
%button.btn-edit-markdown{type: "button",
|
|
9
|
+
onclick: "openMarkdownEditor('#{field.key}')",
|
|
10
|
+
title: "Open rich text editor"}
|
|
11
|
+
%i.iconoir-edit-pencil
|
|
12
|
+
Edit
|
|
13
|
+
|
|
14
|
+
- if field.select?
|
|
15
|
+
%select{id: field.key,
|
|
16
|
+
name: "annotations[#{field.key}]",
|
|
17
|
+
"aria-invalid": error ? "true" : nil}
|
|
18
|
+
%option{value: "", selected: value.nil? || value.empty?} Select...
|
|
19
|
+
- field.options.each do |opt|
|
|
20
|
+
%option{value: opt, selected: value == opt}= opt
|
|
21
|
+
|
|
22
|
+
- elsif field.textarea?
|
|
23
|
+
.markdown-field
|
|
24
|
+
%textarea{id: field.key,
|
|
25
|
+
name: "annotations[#{field.key}]",
|
|
26
|
+
"aria-invalid": error ? "true" : nil,
|
|
27
|
+
placeholder: field.markdown? ? "Enter markdown content..." : "One entry per line..."}= value
|
|
28
|
+
- if field.markdown?
|
|
29
|
+
%small.future-hint
|
|
30
|
+
%i.iconoir-info-circle
|
|
31
|
+
Supports Markdown formatting
|
|
32
|
+
|
|
33
|
+
- elsif field.code?
|
|
34
|
+
.code-field
|
|
35
|
+
%textarea{id: field.key,
|
|
36
|
+
name: "annotations[#{field.key}]",
|
|
37
|
+
"aria-invalid": error ? "true" : nil,
|
|
38
|
+
placeholder: "Enter #{field.code_language} code...",
|
|
39
|
+
spellcheck: "false"}= value
|
|
40
|
+
%small.future-hint
|
|
41
|
+
%i.iconoir-code
|
|
42
|
+
= "#{field.code_language.to_s.capitalize} code"
|
|
43
|
+
|
|
44
|
+
- elsif field.number?
|
|
45
|
+
%input{id: field.key,
|
|
46
|
+
type: "number",
|
|
47
|
+
name: "annotations[#{field.key}]",
|
|
48
|
+
value: value,
|
|
49
|
+
step: field.step,
|
|
50
|
+
"aria-invalid": error ? "true" : nil}
|
|
51
|
+
|
|
52
|
+
- elsif field.url?
|
|
53
|
+
%input{id: field.key,
|
|
54
|
+
type: "url",
|
|
55
|
+
name: "annotations[#{field.key}]",
|
|
56
|
+
value: value,
|
|
57
|
+
placeholder: "https://...",
|
|
58
|
+
"aria-invalid": error ? "true" : nil}
|
|
59
|
+
|
|
60
|
+
- elsif field.list?
|
|
61
|
+
%input{id: field.key,
|
|
62
|
+
type: "text",
|
|
63
|
+
name: "annotations[#{field.key}]",
|
|
64
|
+
value: value,
|
|
65
|
+
placeholder: "Comma-separated values",
|
|
66
|
+
"aria-invalid": error ? "true" : nil}
|
|
67
|
+
|
|
68
|
+
- else
|
|
69
|
+
%input{id: field.key,
|
|
70
|
+
type: "text",
|
|
71
|
+
name: "annotations[#{field.key}]",
|
|
72
|
+
value: value,
|
|
73
|
+
"aria-invalid": error ? "true" : nil}
|
|
74
|
+
|
|
75
|
+
- if field.description && !field.textarea? && !field.code?
|
|
76
|
+
%small.field-description= field.description
|
|
77
|
+
|
|
78
|
+
- if error
|
|
79
|
+
.field-error
|
|
80
|
+
= error.join(", ")
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
- action_url = @mode == :edit ? "/kinds/#{@kind}/instances/#{@instance_name}/generate" : "/kinds/#{@kind}/generate"
|
|
2
|
+
- title = @mode == :edit ? "Edit #{@kind}" : "New #{@kind}"
|
|
3
|
+
- back_url = @mode == :edit ? "/kinds/#{@kind}/instances/#{@instance_name}" : "/kinds/#{@kind}"
|
|
4
|
+
|
|
5
|
+
- if @generated_yaml
|
|
6
|
+
%article.yaml-output
|
|
7
|
+
%header
|
|
8
|
+
%h3
|
|
9
|
+
%i{class: "iconoir-#{@klass.icon} icon-#{@klass.layer}"}
|
|
10
|
+
= title
|
|
11
|
+
%a.btn-back{href: back_url}
|
|
12
|
+
%i.iconoir-arrow-left
|
|
13
|
+
Back
|
|
14
|
+
!= haml :'partials/editor/_yaml_output', locals: { yaml: @generated_yaml, path_ref: @path_ref, kind: @kind, name: @name, content_hash: @content_hash }
|
|
15
|
+
- else
|
|
16
|
+
%form.editor-form{method: "post", action: action_url, "data-kind": @kind}
|
|
17
|
+
/ Metadata
|
|
18
|
+
%article
|
|
19
|
+
%header
|
|
20
|
+
%h3
|
|
21
|
+
%i{class: "iconoir-#{@klass.icon} icon-#{@klass.layer}"}
|
|
22
|
+
= title
|
|
23
|
+
%a.btn-back{href: back_url}
|
|
24
|
+
%i.iconoir-arrow-left
|
|
25
|
+
Back
|
|
26
|
+
- if @mode == :edit
|
|
27
|
+
/ Show name as disabled field in edit mode
|
|
28
|
+
.field-group
|
|
29
|
+
%input#name{type: "text",
|
|
30
|
+
name: "name_display",
|
|
31
|
+
placeholder: "Name",
|
|
32
|
+
value: @name,
|
|
33
|
+
disabled: true}
|
|
34
|
+
%input{type: "hidden", name: "name", value: @name}
|
|
35
|
+
- if @content_hash
|
|
36
|
+
%input{type: "hidden", name: "content_hash", value: @content_hash}
|
|
37
|
+
- else
|
|
38
|
+
/ Editable name field for new resources
|
|
39
|
+
.field-group
|
|
40
|
+
%label{for: "name"}
|
|
41
|
+
Name
|
|
42
|
+
%span.required *
|
|
43
|
+
%input#name{type: "text",
|
|
44
|
+
name: "name",
|
|
45
|
+
value: @name,
|
|
46
|
+
required: true,
|
|
47
|
+
placeholder: "Enter resource name (no spaces)",
|
|
48
|
+
pattern: "\\S+",
|
|
49
|
+
"aria-invalid": @errors["name"] ? "true" : nil}
|
|
50
|
+
- if @errors["name"]
|
|
51
|
+
.field-error
|
|
52
|
+
= @errors["name"].join(", ")
|
|
53
|
+
|
|
54
|
+
/ Annotations
|
|
55
|
+
%article
|
|
56
|
+
%header
|
|
57
|
+
%h3 Annotations
|
|
58
|
+
.annotations-grid
|
|
59
|
+
- @fields.each do |field|
|
|
60
|
+
!= haml :'partials/editor/_field', locals: { field: field, value: @annotations[field.key], error: @errors[field.key] }
|
|
61
|
+
|
|
62
|
+
/ Relations
|
|
63
|
+
%article
|
|
64
|
+
%header
|
|
65
|
+
%h3 Relations
|
|
66
|
+
!= haml :'partials/editor/_relations', locals: { relations: @relations, kind: @kind }
|
|
67
|
+
|
|
68
|
+
/ Submit
|
|
69
|
+
.form-actions
|
|
70
|
+
%button{type: "submit"}
|
|
71
|
+
%i.iconoir-code
|
|
72
|
+
Generate YAML
|
|
73
|
+
%a.secondary{role: "button", href: @mode == :edit ? "/kinds/#{@kind}/instances/#{@instance_name}" : "/kinds/#{@kind}"}
|
|
74
|
+
Cancel
|
|
75
|
+
|
|
76
|
+
/ Markdown Editor Overlay
|
|
77
|
+
#markdown-editor-overlay.hidden
|
|
78
|
+
.markdown-editor-backdrop{onclick: "closeMarkdownEditor()"}
|
|
79
|
+
.markdown-editor-panel
|
|
80
|
+
.lexical-toolbar
|
|
81
|
+
%button.toolbar-btn{type: "button", onclick: "formatLexical('bold')", title: "Bold (Ctrl+B)"}
|
|
82
|
+
%i.iconoir-bold
|
|
83
|
+
%button.toolbar-btn{type: "button", onclick: "formatLexical('italic')", title: "Italic (Ctrl+I)"}
|
|
84
|
+
%i.iconoir-italic
|
|
85
|
+
%button.toolbar-btn{type: "button", onclick: "formatLexical('strikethrough')", title: "Strikethrough"}
|
|
86
|
+
%i.iconoir-strikethrough
|
|
87
|
+
%button.toolbar-btn{type: "button", onclick: "formatLexical('code')", title: "Inline Code"}
|
|
88
|
+
%i.iconoir-code
|
|
89
|
+
.toolbar-divider
|
|
90
|
+
%button.toolbar-btn{type: "button", onclick: "formatLexicalBlock('h1')", title: "Heading 1"}
|
|
91
|
+
H1
|
|
92
|
+
%button.toolbar-btn{type: "button", onclick: "formatLexicalBlock('h2')", title: "Heading 2"}
|
|
93
|
+
H2
|
|
94
|
+
%button.toolbar-btn{type: "button", onclick: "formatLexicalBlock('h3')", title: "Heading 3"}
|
|
95
|
+
H3
|
|
96
|
+
.toolbar-divider
|
|
97
|
+
%button.toolbar-btn{type: "button", onclick: "formatLexicalBlock('bullet')", title: "Bullet List"}
|
|
98
|
+
%i.iconoir-list
|
|
99
|
+
%button.toolbar-btn{type: "button", onclick: "formatLexicalBlock('number')", title: "Numbered List"}
|
|
100
|
+
%i.iconoir-numbered-list-left
|
|
101
|
+
%button.toolbar-btn{type: "button", onclick: "formatLexicalBlock('quote')", title: "Quote"}
|
|
102
|
+
%i.iconoir-quote
|
|
103
|
+
.toolbar-divider
|
|
104
|
+
%button.toolbar-btn{type: "button", onclick: "insertLink()", title: "Insert Link"}
|
|
105
|
+
%i.iconoir-link
|
|
106
|
+
.toolbar-divider
|
|
107
|
+
%button.toolbar-btn{type: "button", onclick: "insertCodeBlock()", title: "Code Block"}
|
|
108
|
+
%i.iconoir-code-brackets
|
|
109
|
+
%select#code-language-picker.toolbar-select.hidden{onchange: "applyCodeLanguage(this.value)", title: "Code Language"}
|
|
110
|
+
%option{value: ""} Language...
|
|
111
|
+
%option{value: "javascript"} JavaScript
|
|
112
|
+
%option{value: "typescript"} TypeScript
|
|
113
|
+
%option{value: "ruby"} Ruby
|
|
114
|
+
%option{value: "python"} Python
|
|
115
|
+
%option{value: "html"} HTML
|
|
116
|
+
%option{value: "css"} CSS
|
|
117
|
+
%option{value: "json"} JSON
|
|
118
|
+
%option{value: "yaml"} YAML
|
|
119
|
+
%option{value: "bash"} Bash
|
|
120
|
+
%option{value: "sql"} SQL
|
|
121
|
+
%option{value: "go"} Go
|
|
122
|
+
%option{value: "rust"} Rust
|
|
123
|
+
%option{value: "java"} Java
|
|
124
|
+
%option{value: "mermaid"} Mermaid
|
|
125
|
+
%option{value: "markdown"} Markdown
|
|
126
|
+
%option{value: "text"} Plain Text
|
|
127
|
+
.toolbar-spacer
|
|
128
|
+
%button.secondary.toolbar-action{type: "button", onclick: "closeMarkdownEditor()"} Cancel
|
|
129
|
+
%button.toolbar-action{type: "button", onclick: "saveMarkdownEditor()"} Save
|
|
130
|
+
.lexical-editor-container
|
|
131
|
+
#lexical-editor-root{contenteditable: "true"}
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
- verbs = relation_verbs(kind)
|
|
2
|
+
- all_relations = available_relations(kind)
|
|
3
|
+
- verb_kind_options = all_relations.map { |v, _, k| ["#{v}:#{k}", v, k] }.sort_by(&:first).uniq
|
|
4
|
+
- target_kinds = verb_kind_options.map { |_, _, k| k }.uniq
|
|
5
|
+
- instances_by_kind = target_kinds.each_with_object({}) { |k, h| h[k] = db.instances_by_kind(k).keys.sort rescue [] }
|
|
6
|
+
|
|
7
|
+
.relations-editor{"data-kind": kind, "data-instances": instances_by_kind.to_json}
|
|
8
|
+
- if verb_kind_options.empty?
|
|
9
|
+
%p.no-relations This resource type has no defined relations.
|
|
10
|
+
- else
|
|
11
|
+
/ Existing relations list (read-only display with hidden inputs)
|
|
12
|
+
#relations-list
|
|
13
|
+
- if relations&.any?
|
|
14
|
+
- sorted_relations = relations.sort_by { |r| [r[:verb].to_s, r[:kind].to_s, r[:name].to_s] }
|
|
15
|
+
- sorted_relations.each do |rel|
|
|
16
|
+
.relation-item
|
|
17
|
+
%span.relation-text
|
|
18
|
+
%strong= rel[:verb]
|
|
19
|
+
\→ #{rel[:kind]}:
|
|
20
|
+
%em= rel[:name]
|
|
21
|
+
%input{type: "hidden", name: "relations[][verb]", value: rel[:verb]}
|
|
22
|
+
%input{type: "hidden", name: "relations[][kind]", value: rel[:kind]}
|
|
23
|
+
%input{type: "hidden", name: "relations[][name]", value: rel[:name]}
|
|
24
|
+
%button.btn-remove{type: "button", onclick: "removeRelation(this)", title: "Remove"}
|
|
25
|
+
%i.iconoir-xmark
|
|
26
|
+
|
|
27
|
+
/ Add relation form - inline with verb:kind and instance dropdowns
|
|
28
|
+
.add-relation-row
|
|
29
|
+
%select#new-relation-verb-kind{onchange: "updateInstanceOptions()"}
|
|
30
|
+
%option{value: ""} Select relation...
|
|
31
|
+
- verb_kind_options.each do |combo, verb, target_kind|
|
|
32
|
+
%option{value: combo, "data-verb": verb, "data-kind": target_kind}= "#{verb} \u2192 #{target_kind}"
|
|
33
|
+
|
|
34
|
+
%select#new-relation-instance
|
|
35
|
+
%option{value: ""} Select instance...
|
|
36
|
+
|
|
37
|
+
%button.secondary.btn-add{type: "button", onclick: "addRelation()"}
|
|
38
|
+
%i.iconoir-plus
|
|
39
|
+
Add
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
%p.yaml-success
|
|
2
|
+
%i.iconoir-check-circle
|
|
3
|
+
Generated YAML
|
|
4
|
+
|
|
5
|
+
%pre#yaml-content
|
|
6
|
+
%code.language-yaml= yaml
|
|
7
|
+
|
|
8
|
+
%footer
|
|
9
|
+
- if defined?(path_ref) && path_ref
|
|
10
|
+
%p.yaml-file-info
|
|
11
|
+
%i.iconoir-folder
|
|
12
|
+
%span
|
|
13
|
+
Source: #{path_ref.path}:#{path_ref.line_no}
|
|
14
|
+
- else
|
|
15
|
+
%p.yaml-instructions
|
|
16
|
+
%i.iconoir-info-circle
|
|
17
|
+
%span
|
|
18
|
+
Copy this YAML and save it to a
|
|
19
|
+
%code .yaml
|
|
20
|
+
file in your resources directory,
|
|
21
|
+
%br
|
|
22
|
+
then reload the application.
|
|
23
|
+
.yaml-actions
|
|
24
|
+
- if defined?(path_ref) && path_ref && inline_edit_enabled?
|
|
25
|
+
%button#save-yaml{type: "button", data: { kind: kind, name: name, "content-hash": content_hash }}
|
|
26
|
+
%i.iconoir-check-circle
|
|
27
|
+
Save to File
|
|
28
|
+
%button#copy-yaml{type: "button", "data-clipboard-target": "#yaml-content"}
|
|
29
|
+
%i.iconoir-copy
|
|
30
|
+
Copy to Clipboard
|
|
31
|
+
%a.secondary{role: "button", href: "javascript:history.back()"}
|
|
32
|
+
%i.iconoir-edit-pencil
|
|
33
|
+
Edit Again
|
|
@@ -4,7 +4,6 @@
|
|
|
4
4
|
- description = analysis.annotations['analysis/description']
|
|
5
5
|
- handler = analysis.annotations['analysis/handler'] || 'ruby'
|
|
6
6
|
- timeout = analysis.annotations['analysis/timeout'] || '30s'
|
|
7
|
-
- enabled = analysis.annotations['analysis/enabled'] != 'false'
|
|
8
7
|
|
|
9
8
|
%article.analysis-header
|
|
10
9
|
%header
|
|
@@ -13,8 +12,10 @@
|
|
|
13
12
|
.instance-title-text
|
|
14
13
|
%span.instance-name= @instance
|
|
15
14
|
%span.instance-kind-subtitle= @kind
|
|
16
|
-
-
|
|
17
|
-
%
|
|
15
|
+
.header-actions
|
|
16
|
+
%a.btn-header{href: "/kinds/#{@kind}/instances/#{@instance}/edit", title: "Edit this resource"}
|
|
17
|
+
%i.iconoir-edit-pencil
|
|
18
|
+
Edit
|
|
18
19
|
- if description
|
|
19
20
|
.analysis-description
|
|
20
21
|
!= markdown(description)
|
|
@@ -49,14 +50,6 @@
|
|
|
49
50
|
%span.analysis-meta-item
|
|
50
51
|
%i.iconoir-timer
|
|
51
52
|
= timeout
|
|
52
|
-
- if enabled
|
|
53
|
-
%span.analysis-meta-item.status-enabled
|
|
54
|
-
%i.iconoir-check-circle
|
|
55
|
-
Enabled
|
|
56
|
-
- else
|
|
57
|
-
%span.analysis-meta-item.status-disabled
|
|
58
|
-
%i.iconoir-xmark-circle
|
|
59
|
-
Disabled
|
|
60
53
|
.analysis-script
|
|
61
54
|
.analysis-script-header
|
|
62
55
|
%strong Script
|
|
@@ -28,6 +28,10 @@
|
|
|
28
28
|
%span.generated-time
|
|
29
29
|
generated
|
|
30
30
|
= time_ago(instance.annotations['generated/at'])
|
|
31
|
+
- else
|
|
32
|
+
%a.btn-header{href: "/kinds/#{@kind}/instances/#{@instance}/edit", title: "Edit this resource"}
|
|
33
|
+
%i.iconoir-edit-pencil
|
|
34
|
+
Edit
|
|
31
35
|
- if instance.has_relations?
|
|
32
36
|
.graph-container
|
|
33
37
|
#graphviz.canvas
|
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
- if @doc_content
|
|
3
3
|
%article
|
|
4
4
|
!= @doc_content
|
|
5
|
+
- elsif @editor_mode
|
|
6
|
+
!= haml :'partials/editor/_form'
|
|
5
7
|
- elsif @instances
|
|
6
8
|
!= haml :search
|
|
7
9
|
- elsif @kind && @instance
|
|
@@ -37,8 +39,12 @@
|
|
|
37
39
|
%h2
|
|
38
40
|
%i{class: "iconoir-#{Archsight::Resources[@kind].icon} icon-#{Archsight::Resources[@kind].layer}"}
|
|
39
41
|
= @kind
|
|
40
|
-
|
|
41
|
-
%
|
|
42
|
+
.header-actions
|
|
43
|
+
%a.kind-help{href: "/doc/resources/#{kind_snake}", title: "Documentation for #{@kind}"}
|
|
44
|
+
%i.iconoir-help-circle
|
|
45
|
+
%a.btn-header{href: "/kinds/#{@kind}/new", title: "Create new #{@kind}"}
|
|
46
|
+
%i.iconoir-plus
|
|
47
|
+
New
|
|
42
48
|
!= haml :"partials/instance/_list", locals: { instances: db.instances_by_kind(@kind).values.sort_by(&:name), omit_kind: true }
|
|
43
49
|
- elsif !@kind
|
|
44
50
|
!= haml :"partials/instance/_graph"
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
%link{href: asset_path('/css/highlight.min.css'), rel: 'stylesheet'}/
|
|
13
13
|
%link{href: asset_path('/css/mermaid-layers.css'), rel: 'stylesheet'}/
|
|
14
14
|
%link{href: asset_path('/css/graph.css'), rel: 'stylesheet'}/
|
|
15
|
+
%link{href: asset_path('/css/editor.css'), rel: 'stylesheet'}/
|
|
15
16
|
%script{src: asset_path('/js/htmx.min.js')}
|
|
16
17
|
%script{src: asset_path('/js/mermaid.min.js')}
|
|
17
18
|
%script{src: asset_path('/js/svg-pan-zoom.min.js')}
|
|
@@ -20,5 +21,6 @@
|
|
|
20
21
|
%script{src: asset_path("/js/mermaid-init.js")}
|
|
21
22
|
%script{ src: asset_path("/js/highlight.min.js") }
|
|
22
23
|
%script{ src: asset_path("/js/sparkline.js") }
|
|
24
|
+
%script{ src: asset_path("/js/editor.js"), type: "module" }
|
|
23
25
|
:javascript
|
|
24
26
|
hljs.highlightAll();
|