first version
This commit is contained in:
@@ -1,13 +1,18 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
<Project Sdk="Microsoft.NET.Sdk.Web">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net10.0</TargetFramework>
|
||||
<Nullable>enable</Nullable>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<UserSecretsId>ff020750-eacc-49ce-880e-1ecbc30605ac</UserSecretsId>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.AspNetCore.OpenApi" Version="10.0.0" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\S8n.Components.Packages\S8n.Components.Packages.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -1,3 +1,9 @@
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
|
||||
// Ensure S8n.Components.Packages assembly is loaded
|
||||
_ = typeof(S8n.Components.Basics.Calculator).Assembly;
|
||||
|
||||
var builder = WebApplication.CreateBuilder(args);
|
||||
|
||||
// Add services to the container.
|
||||
@@ -33,9 +39,126 @@ app.MapGet("/weatherforecast", () =>
|
||||
})
|
||||
.WithName("GetWeatherForecast");
|
||||
|
||||
// Runtime execution endpoint
|
||||
app.MapPost("/api/runtime/execute", (RuntimeRequest request) =>
|
||||
{
|
||||
try
|
||||
{
|
||||
// Get the type from the class name
|
||||
var type = Type.GetType(request.ClassName);
|
||||
if (type == null)
|
||||
{
|
||||
// Try to find in loaded assemblies
|
||||
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||||
{
|
||||
type = assembly.GetType(request.ClassName);
|
||||
if (type != null) break;
|
||||
}
|
||||
}
|
||||
|
||||
if (type == null)
|
||||
{
|
||||
return Results.BadRequest(new RuntimeResponse
|
||||
{
|
||||
Error = $"Class not found: {request.ClassName}"
|
||||
});
|
||||
}
|
||||
|
||||
// Create instance
|
||||
var instance = Activator.CreateInstance(type);
|
||||
if (instance == null)
|
||||
{
|
||||
return Results.BadRequest(new RuntimeResponse
|
||||
{
|
||||
Error = $"Failed to create instance of: {request.ClassName}"
|
||||
});
|
||||
}
|
||||
|
||||
// Get the method
|
||||
var method = type.GetMethod(request.MethodName);
|
||||
if (method == null)
|
||||
{
|
||||
return Results.BadRequest(new RuntimeResponse
|
||||
{
|
||||
Error = $"Method not found: {request.MethodName}"
|
||||
});
|
||||
}
|
||||
|
||||
// Parse inputs and invoke method
|
||||
var inputs = request.Inputs as JsonElement?;
|
||||
object? result;
|
||||
|
||||
if (inputs.HasValue)
|
||||
{
|
||||
// Get method parameters
|
||||
var parameters = method.GetParameters();
|
||||
var args = new List<object?>();
|
||||
|
||||
foreach (var param in parameters)
|
||||
{
|
||||
if (inputs.Value.TryGetProperty(param.Name!, out var property))
|
||||
{
|
||||
var value = JsonSerializer.Deserialize(property.GetRawText(), param.ParameterType);
|
||||
args.Add(value);
|
||||
}
|
||||
else if (param.IsOptional)
|
||||
{
|
||||
args.Add(param.DefaultValue);
|
||||
}
|
||||
else
|
||||
{
|
||||
return Results.BadRequest(new RuntimeResponse
|
||||
{
|
||||
Error = $"Missing required parameter: {param.Name}"
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
result = method.Invoke(instance, args.ToArray());
|
||||
}
|
||||
else
|
||||
{
|
||||
result = method.Invoke(instance, null);
|
||||
}
|
||||
|
||||
return Results.Ok(new RuntimeResponse
|
||||
{
|
||||
Outputs = result
|
||||
});
|
||||
}
|
||||
catch (TargetInvocationException tie) when (tie.InnerException != null)
|
||||
{
|
||||
return Results.BadRequest(new RuntimeResponse
|
||||
{
|
||||
Error = tie.InnerException.Message
|
||||
});
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
return Results.BadRequest(new RuntimeResponse
|
||||
{
|
||||
Error = ex.Message
|
||||
});
|
||||
}
|
||||
})
|
||||
.WithName("ExecuteRuntime");
|
||||
|
||||
app.Run();
|
||||
|
||||
record WeatherForecast(DateOnly Date, int TemperatureC, string? Summary)
|
||||
{
|
||||
public int TemperatureF => 32 + (int)(TemperatureC / 0.5556);
|
||||
}
|
||||
|
||||
record RuntimeRequest
|
||||
{
|
||||
public string ClassName { get; set; } = string.Empty;
|
||||
public string MethodName { get; set; } = string.Empty;
|
||||
public object? Inputs { get; set; }
|
||||
}
|
||||
|
||||
record RuntimeResponse
|
||||
{
|
||||
public object? Outputs { get; set; }
|
||||
public string? Error { get; set; }
|
||||
}
|
||||
|
||||
@@ -20,7 +20,7 @@ export default defineConfig((ctx) => {
|
||||
// https://github.com/quasarframework/quasar/tree/dev/extras
|
||||
extras: [
|
||||
// 'ionicons-v4',
|
||||
// 'mdi-v7',
|
||||
'mdi-v7',
|
||||
// 'fontawesome-v6',
|
||||
// 'eva-icons',
|
||||
// 'themify',
|
||||
@@ -99,6 +99,9 @@ export default defineConfig((ctx) => {
|
||||
devServer: {
|
||||
// https: true,
|
||||
open: true, // opens browser window automatically
|
||||
proxy: {
|
||||
'/api': 'http://localhost:5127',
|
||||
},
|
||||
},
|
||||
|
||||
// https://v2.quasar.dev/quasar-cli-vite/quasar-config-file#framework
|
||||
|
||||
@@ -1,19 +1,83 @@
|
||||
import { ref } from 'vue';
|
||||
import { type Ref, ref, shallowRef } from 'vue';
|
||||
|
||||
const API_BASE_URL = ''; // Backend API URL
|
||||
|
||||
export interface RuntimeExecutor<TIn, TOut> {
|
||||
execute(this: void, method: string): Promise<TOut | undefined>;
|
||||
running: Ref<boolean>;
|
||||
inputs: Ref<TIn>;
|
||||
outputs: Ref<TOut | undefined>;
|
||||
error: Ref<string | undefined>;
|
||||
duration: Ref<number>;
|
||||
}
|
||||
|
||||
export const runtime = {
|
||||
createExecutor(className: string) {
|
||||
const inputs = ref();
|
||||
const outputs = ref();
|
||||
return {
|
||||
execute(method: string): Promise<unknown> {
|
||||
console.log('executing...', className, method);
|
||||
createExecutor<TIn, TOut>(
|
||||
className: string,
|
||||
initInputs: TIn,
|
||||
silentErrors = true,
|
||||
): RuntimeExecutor<TIn, TOut> {
|
||||
const inputs = ref<TIn>(initInputs) as Ref<TIn>;
|
||||
const outputs = ref<TOut>();
|
||||
const duration = shallowRef(0);
|
||||
const running = shallowRef(false);
|
||||
const error = shallowRef<string>();
|
||||
|
||||
return new Promise((r) => {
|
||||
setTimeout(() => r(null), 2500);
|
||||
});
|
||||
const execute = async (method: string): Promise<TOut | undefined> => {
|
||||
console.trace('executing...', className, method);
|
||||
const startTime = performance.now();
|
||||
try {
|
||||
error.value = undefined;
|
||||
running.value = true;
|
||||
duration.value = 0;
|
||||
const response = await fetch(`${API_BASE_URL}/api/runtime/execute`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
},
|
||||
body: JSON.stringify({
|
||||
className,
|
||||
methodName: method,
|
||||
inputs: inputs.value,
|
||||
}),
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorData = await response.json();
|
||||
throw new Error(errorData.error || `HTTP error! status: ${response.status}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
|
||||
if (data.error) {
|
||||
throw new Error(data.error);
|
||||
}
|
||||
|
||||
outputs.value = data.outputs || {};
|
||||
return outputs.value;
|
||||
} catch (err) {
|
||||
console.error('Runtime execution error:', err);
|
||||
error.value = err instanceof Error ? err.message : 'An error occurred';
|
||||
|
||||
if (!silentErrors) {
|
||||
throw err;
|
||||
}
|
||||
} finally {
|
||||
running.value = false;
|
||||
const endTime = performance.now();
|
||||
duration.value = Math.round((endTime - startTime) * 10) / 10;
|
||||
}
|
||||
return undefined;
|
||||
};
|
||||
|
||||
return {
|
||||
execute,
|
||||
|
||||
running,
|
||||
duration,
|
||||
inputs,
|
||||
outputs,
|
||||
error,
|
||||
};
|
||||
},
|
||||
};
|
||||
|
||||
96
QuasarFrontend/src/components_s8n/ComponentCalculator.vue
Normal file
96
QuasarFrontend/src/components_s8n/ComponentCalculator.vue
Normal file
@@ -0,0 +1,96 @@
|
||||
<template>
|
||||
<q-card class="s8n-calculator q-pa-md">
|
||||
<q-card-section>
|
||||
<div class="text-h6">Simple Calculator</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-section class="q-gutter-md">
|
||||
<q-select
|
||||
outlined
|
||||
v-model="inputs.operator"
|
||||
label="Operator"
|
||||
:options="[
|
||||
{ label: 'Add (+)', value: 'add' },
|
||||
{ label: 'Subtract (-)', value: 'subtract' },
|
||||
{ label: 'Multiply (×)', value: 'multiply' },
|
||||
{ label: 'Divide (÷)', value: 'divide' },
|
||||
]"
|
||||
emit-value
|
||||
map-options
|
||||
/>
|
||||
<q-input
|
||||
v-for="(a, aidx) in inputs.args"
|
||||
:key="aidx"
|
||||
outlined
|
||||
v-model.number="inputs.args[aidx]"
|
||||
:label="`${aidx === 0 ? 'Base' : aidx === 1 ? 'Second' : aidx === 2 ? 'Third' : `${aidx + 1}th`} Number`"
|
||||
type="number"
|
||||
>
|
||||
<template v-slot:after>
|
||||
<q-btn
|
||||
v-if="aidx > 1"
|
||||
@click="inputs.args.splice(aidx, 1)"
|
||||
class="col-auto"
|
||||
icon="mdi-delete"
|
||||
color="negative"
|
||||
outline
|
||||
dense
|
||||
></q-btn>
|
||||
</template>
|
||||
</q-input>
|
||||
|
||||
<q-btn icon="mdi-plus" @click="inputs.args[inputs.args.length] = 0"></q-btn>
|
||||
|
||||
<div class="result-section q-mt-md">
|
||||
<q-banner v-if="error" class="bg-negative text-white" rounded>
|
||||
{{ error }}
|
||||
</q-banner>
|
||||
<div v-else-if="outputs !== undefined" class="text-h5 text-primary">
|
||||
Result: {{ outputs.result }} ({{ duration }}ms)
|
||||
</div>
|
||||
</div>
|
||||
</q-card-section>
|
||||
|
||||
<q-card-actions align="right">
|
||||
<q-btn
|
||||
color="primary"
|
||||
@click="execute('Calc')"
|
||||
:loading="running"
|
||||
size="lg"
|
||||
icon="calculate"
|
||||
label="Calculate"
|
||||
/>
|
||||
</q-card-actions>
|
||||
</q-card>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { runtime } from '../components/s8n-runtime';
|
||||
|
||||
const { execute, running, error, duration, outputs, inputs } = runtime.createExecutor<
|
||||
{
|
||||
operator: string;
|
||||
args: number[];
|
||||
},
|
||||
{
|
||||
result?: number;
|
||||
}
|
||||
>('S8n.Components.Basics.Calculator', {
|
||||
operator: 'add',
|
||||
args: [0, 0],
|
||||
});
|
||||
</script>
|
||||
|
||||
<style lang="scss" scoped>
|
||||
.s8n-calculator {
|
||||
max-width: 400px;
|
||||
width: 100%;
|
||||
|
||||
.result-section {
|
||||
min-height: 60px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,43 +1,9 @@
|
||||
<template>
|
||||
<q-page class="row items-center justify-evenly">
|
||||
<example-component
|
||||
title="Example component"
|
||||
active
|
||||
:todos="todos"
|
||||
:meta="meta"
|
||||
></example-component>
|
||||
<component-calculator />
|
||||
</q-page>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue';
|
||||
import type { Todo, Meta } from 'components/models';
|
||||
import ExampleComponent from 'components/ExampleComponent.vue';
|
||||
|
||||
const todos = ref<Todo[]>([
|
||||
{
|
||||
id: 1,
|
||||
content: 'ct1',
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
content: 'ct2',
|
||||
},
|
||||
{
|
||||
id: 3,
|
||||
content: 'ct3',
|
||||
},
|
||||
{
|
||||
id: 4,
|
||||
content: 'ct4',
|
||||
},
|
||||
{
|
||||
id: 5,
|
||||
content: 'ct5',
|
||||
},
|
||||
]);
|
||||
|
||||
const meta = ref<Meta>({
|
||||
totalCount: 1200,
|
||||
});
|
||||
import ComponentCalculator from 'components/ComponentCalculator.vue';
|
||||
</script>
|
||||
|
||||
61
S8n.Components.Packages/Basics/Calculator.cs
Normal file
61
S8n.Components.Packages/Basics/Calculator.cs
Normal file
@@ -0,0 +1,61 @@
|
||||
using System.Text.Json;
|
||||
|
||||
namespace S8n.Components.Basics;
|
||||
|
||||
public class Calculator
|
||||
{
|
||||
public object Calc(string @operator, object[] args)
|
||||
{
|
||||
if (args == null || args.Length < 2)
|
||||
{
|
||||
throw new ArgumentException("At least two arguments are required");
|
||||
}
|
||||
|
||||
// Convert arguments to decimal for precision, handling JsonElement
|
||||
var numbers = args.Select(a =>
|
||||
{
|
||||
if (a is JsonElement jsonElement)
|
||||
{
|
||||
return jsonElement.GetDecimal();
|
||||
}
|
||||
return Convert.ToDecimal(a);
|
||||
}).ToArray();
|
||||
decimal result = 0;
|
||||
|
||||
switch (@operator?.ToLower())
|
||||
{
|
||||
case "add":
|
||||
result = numbers.Sum();
|
||||
break;
|
||||
case "subtract":
|
||||
result = numbers[0];
|
||||
for (int i = 1; i < numbers.Length; i++)
|
||||
{
|
||||
result -= numbers[i];
|
||||
}
|
||||
break;
|
||||
case "multiply":
|
||||
result = numbers[0];
|
||||
for (int i = 1; i < numbers.Length; i++)
|
||||
{
|
||||
result *= numbers[i];
|
||||
}
|
||||
break;
|
||||
case "divide":
|
||||
result = numbers[0];
|
||||
for (int i = 1; i < numbers.Length; i++)
|
||||
{
|
||||
if (numbers[i] == 0)
|
||||
{
|
||||
throw new DivideByZeroException("Division by zero is not allowed");
|
||||
}
|
||||
result /= numbers[i];
|
||||
}
|
||||
break;
|
||||
default:
|
||||
throw new ArgumentException($"Unknown operator: {@operator}");
|
||||
}
|
||||
|
||||
return new { Result = result };
|
||||
}
|
||||
}
|
||||
9
S8n.Components.Packages/S8n.Components.Packages.csproj
Normal file
9
S8n.Components.Packages/S8n.Components.Packages.csproj
Normal file
@@ -0,0 +1,9 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net9.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
</PropertyGroup>
|
||||
|
||||
</Project>
|
||||
@@ -9,6 +9,8 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyCompany.MyProject.Models"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "MyCompany.MyProject.Tests", "MyCompany.MyProject.Tests\MyCompany.MyProject.Tests.csproj", "{A4D2D9E2-8A84-4F3F-8A16-0F83FD8DDC87}"
|
||||
EndProject
|
||||
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "S8n.Components.Packages", "S8n.Components.Packages\S8n.Components.Packages.csproj", "{B5C8D5E2-3A4B-4C6D-8E9F-0A1B2C3D4E5F}"
|
||||
EndProject
|
||||
Global
|
||||
GlobalSection(SolutionConfigurationPlatforms) = preSolution
|
||||
Debug|Any CPU = Debug|Any CPU
|
||||
@@ -55,6 +57,18 @@ Global
|
||||
{A4D2D9E2-8A84-4F3F-8A16-0F83FD8DDC87}.Release|x64.Build.0 = Release|Any CPU
|
||||
{A4D2D9E2-8A84-4F3F-8A16-0F83FD8DDC87}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{A4D2D9E2-8A84-4F3F-8A16-0F83FD8DDC87}.Release|x86.Build.0 = Release|Any CPU
|
||||
{B5C8D5E2-3A4B-4C6D-8E9F-0A1B2C3D4E5F}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
|
||||
{B5C8D5E2-3A4B-4C6D-8E9F-0A1B2C3D4E5F}.Debug|Any CPU.Build.0 = Debug|Any CPU
|
||||
{B5C8D5E2-3A4B-4C6D-8E9F-0A1B2C3D4E5F}.Debug|x64.ActiveCfg = Debug|Any CPU
|
||||
{B5C8D5E2-3A4B-4C6D-8E9F-0A1B2C3D4E5F}.Debug|x64.Build.0 = Debug|Any CPU
|
||||
{B5C8D5E2-3A4B-4C6D-8E9F-0A1B2C3D4E5F}.Debug|x86.ActiveCfg = Debug|Any CPU
|
||||
{B5C8D5E2-3A4B-4C6D-8E9F-0A1B2C3D4E5F}.Debug|x86.Build.0 = Debug|Any CPU
|
||||
{B5C8D5E2-3A4B-4C6D-8E9F-0A1B2C3D4E5F}.Release|Any CPU.ActiveCfg = Release|Any CPU
|
||||
{B5C8D5E2-3A4B-4C6D-8E9F-0A1B2C3D4E5F}.Release|Any CPU.Build.0 = Release|Any CPU
|
||||
{B5C8D5E2-3A4B-4C6D-8E9F-0A1B2C3D4E5F}.Release|x64.ActiveCfg = Release|Any CPU
|
||||
{B5C8D5E2-3A4B-4C6D-8E9F-0A1B2C3D4E5F}.Release|x64.Build.0 = Release|Any CPU
|
||||
{B5C8D5E2-3A4B-4C6D-8E9F-0A1B2C3D4E5F}.Release|x86.ActiveCfg = Release|Any CPU
|
||||
{B5C8D5E2-3A4B-4C6D-8E9F-0A1B2C3D4E5F}.Release|x86.Build.0 = Release|Any CPU
|
||||
EndGlobalSection
|
||||
GlobalSection(SolutionProperties) = preSolution
|
||||
HideSolutionNode = FALSE
|
||||
|
||||
Submodule components updated: db90a10bf6...2ccd4e998b
Reference in New Issue
Block a user