Skip to content

Commit 69f6d36

Browse files
committed
feat(Dashboard): select code snippet when a graph node is clicked
Signed-off-by: Jean-Baptiste Bianchi <[email protected]>
1 parent ff68b6b commit 69f6d36

File tree

5 files changed

+102
-5
lines changed

5 files changed

+102
-5
lines changed

src/dashboard/Synapse.Dashboard/Components/WorkflowDiagram/WorkflowDiagramStore.cs

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616
using ServerlessWorkflow.Sdk.Models;
1717
using Synapse.Resources;
1818
using System.Text.RegularExpressions;
19-
using System.Threading.Tasks;
2019

2120
namespace Synapse.Dashboard.Components.WorkflowDiagramStateManagement;
2221

src/dashboard/Synapse.Dashboard/Pages/Workflows/Details/Store.cs

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
// See the License for the specific language governing permissions and
1212
// limitations under the License.
1313

14+
using Neuroglia.Blazor.Dagre.Models;
1415
using ServerlessWorkflow.Sdk.Models;
1516
using Synapse.Api.Client.Services;
1617
using Synapse.Resources;
@@ -26,13 +27,15 @@ namespace Synapse.Dashboard.Pages.Workflows.Details;
2627
/// <param name="monacoEditorHelper">The service used ease Monaco Editor interactions</param>
2728
/// <param name="jsonSerializer">The service used to serialize and deserialize JSON</param>
2829
/// <param name="yamlSerializer">The service used to serialize and deserialize YAML</param>
30+
/// <param name="monacoInterop">The service used to build a bridge with the monaco interop extension</param>
2931
public class WorkflowDetailsStore(
3032
ISynapseApiClient apiClient,
3133
ResourceWatchEventHubClient resourceEventHub,
3234
IJSRuntime jsRuntime,
3335
IMonacoEditorHelper monacoEditorHelper,
3436
IJsonSerializer jsonSerializer,
35-
IYamlSerializer yamlSerializer
37+
IYamlSerializer yamlSerializer,
38+
MonacoInterop monacoInterop
3639
)
3740
: NamespacedResourceManagementComponentStore<WorkflowDetailsState, WorkflowInstance>(apiClient, resourceEventHub)
3841
{
@@ -60,6 +63,11 @@ IYamlSerializer yamlSerializer
6063
/// </summary>
6164
protected IYamlSerializer YamlSerializer { get; } = yamlSerializer;
6265

66+
/// <summary>
67+
/// Gets the service used to build a bridge with the monaco interop extension
68+
/// </summary>
69+
protected MonacoInterop MonacoInterop { get; } = monacoInterop;
70+
6371
/// <summary>
6472
/// Gets/sets the <see cref="BlazorMonaco.Editor.StandaloneEditorConstructionOptions"/> provider function
6573
/// </summary>
@@ -355,6 +363,22 @@ public async Task DeleteWorkflowInstanceAsync(WorkflowInstance workflowInstance)
355363
{
356364
await this.ApiClient.ManageNamespaced<WorkflowInstance>().DeleteAsync(workflowInstance.GetName(), workflowInstance.GetNamespace()!).ConfigureAwait(false);
357365
}
366+
367+
/// <summary>
368+
/// Selects the target node in the code editor
369+
/// </summary>
370+
/// <param name="e">The source of the event</param>
371+
public async Task SelectNodeInEditor(GraphEventArgs<MouseEventArgs> e)
372+
{
373+
if (e.GraphElement == null) return;
374+
if (this.TextEditor == null) return;
375+
var source = await this.TextEditor.GetValue();
376+
var pointer = e.GraphElement.Id;
377+
var language = this.MonacoEditorHelper.PreferredLanguage;
378+
var range = await this.MonacoInterop.GetJsonPointerRange(source, pointer, language);
379+
await this.TextEditor.SetSelection(range, string.Empty);
380+
await this.TextEditor.RevealRangeInCenter(range);
381+
}
358382
#endregion
359383

360384
/// <inheritdoc/>

src/dashboard/Synapse.Dashboard/Pages/Workflows/Details/View.razor

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,7 +51,11 @@
5151
}
5252
else
5353
{
54-
<WorkflowDiagram WorkflowDefinition="@workflowDefinition" WorkflowInstances="@(workflowInstance != null ? [workflowInstance] : (Resources ?? []))" />
54+
<WorkflowDiagram
55+
WorkflowDefinition="@workflowDefinition"
56+
WorkflowInstances="@(workflowInstance != null ? [workflowInstance] : (Resources ?? []))"
57+
OnMouseUp="async (e) => await Store.SelectNodeInEditor(e)"
58+
/>
5559
}
5660
</Content>
5761
</HorizontalCollapsible>

src/dashboard/Synapse.Dashboard/Services/MonacoInterop.cs

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
// See the License for the specific language governing permissions and
1212
// limitations under the License.
1313

14+
using System.Data;
1415
using System.Runtime.CompilerServices;
1516

1617
namespace Synapse.Dashboard.Services;
@@ -29,7 +30,7 @@ public class MonacoInterop(IJSRuntime jsRuntime)
2930
/// <summary>
3031
/// A reference to the js interop module
3132
/// </summary>
32-
readonly Lazy<Task<IJSObjectReference>> moduleTask = new(() => jsRuntime.InvokeAsync<IJSObjectReference>("import", "./js/monaco-editor-interop-extension.js").AsTask());
33+
readonly Lazy<Task<IJSObjectReference>> moduleTask = new(() => jsRuntime.InvokeAsync<IJSObjectReference>("import", "./js/monaco-editor-interop-extension.js?v=2").AsTask());
3334

3435
/// <summary>
3536
/// Adds the provided schema to monaco editor's diagnostics options
@@ -44,6 +45,19 @@ public async ValueTask AddValidationSchemaAsync(string schema, string schemaUri,
4445
await module.InvokeVoidAsync("addValidationSchema", schema, schemaUri, schemaType);
4546
}
4647

48+
/// <summary>
49+
/// Finds the range in a JSON/YAML text corresponding to a provided JSON Pointer
50+
/// </summary>
51+
/// <param name="source">The source JSON/YAML text</param>
52+
/// <param name="jsonPointer">The JSON pointer to find the range for</param>
53+
/// <param name="language">The language of the source, JSON or YAML</param>
54+
/// <returns>The corresponding <see cref="BlazorMonaco.Range"/></returns>
55+
public async ValueTask<BlazorMonaco.Range> GetJsonPointerRange(string source, string jsonPointer, string language)
56+
{
57+
var module = await moduleTask.Value;
58+
return await module.InvokeAsync<BlazorMonaco.Range>("getJsonPointerRange", source, jsonPointer, language);
59+
}
60+
4761
/// <inheritdoc />
4862
public async ValueTask DisposeAsync()
4963
{

src/dashboard/Synapse.Dashboard/wwwroot/js/monaco-editor-interop-extension.js

Lines changed: 57 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1-
/**
1+
import { parse } from "https://esm.sh/json-source-map";
2+
import { LineCounter, parseDocument } from "https://esm.sh/yaml";
3+
4+
/**
25
* Adds a validation schema to monaco editor's diagnostics options
36
* @param {any} schema The validation schema to add
47
* @param {any} schemaUri The schema identifier
@@ -40,4 +43,57 @@ export function addValidationSchema(schema, schemaUri, schemaType) {
4043
]
4144
});
4245
}
46+
}
47+
48+
function getJsonPointeRangeForJson(source, jsonPointer) {
49+
const { pointers } = parse(source);
50+
const node = pointers[jsonPointer];
51+
if (!node) {
52+
throw new Error(`Unable to find JSON pointer '${jsonPointer}'`);
53+
}
54+
return {
55+
startLineNumber: node.key.line + 1,
56+
startColumn: node.key.column + 1,
57+
endLineNumber: node.valueEnd.line + 1,
58+
endColumn: node.valueEnd.column + 1
59+
};
60+
}
61+
function getJsonPointeRangeForYaml(source, jsonPointer) {
62+
const lineCounter = new LineCounter(source);
63+
const document = parseDocument(source, { keepSourceTokens: true, lineCounter });
64+
const node = jsonPointer.split('/').slice(1).reduce((n, key) => {
65+
const idx = parseInt(key, 10);
66+
const nextNode = n.get(!isNaN(idx) ? idx : key, true);
67+
if (!nextNode) {
68+
throw new Error(`Unable to find JSON pointer '${jsonPointer}'`);
69+
}
70+
return nextNode;
71+
}, document);
72+
const start = lineCounter.linePos(node.range[0]);
73+
const end = lineCounter.linePos(node.range[1]);
74+
return {
75+
startLineNumber: start.line,
76+
startColumn: start.col,
77+
endLineNumber: end.line,
78+
endColumn: end.col
79+
}
80+
}
81+
82+
/**
83+
* Finds the range in a JSON/YAML text corresponding to a provided JSON Pointer
84+
* @param {string} source The source JSON/YAML text
85+
* @param {string} jsonPointer The JSON pointer to find the range for
86+
* @param {'json'|'yaml'} language The language of the source, JSON or YAML
87+
* @returns {Monaco.Range} The corresponding range
88+
*/
89+
export function getJsonPointerRange(source, jsonPointer, language) {
90+
if (language.toLowerCase() === 'json') {
91+
return getJsonPointeRangeForJson(source, jsonPointer);
92+
}
93+
else if (language.toLowerCase() === 'yaml') {
94+
return getJsonPointeRangeForYaml(source, jsonPointer);
95+
}
96+
else {
97+
throw new Error(`Invalid language '${language}'`);
98+
}
4399
}

0 commit comments

Comments
 (0)