Add service-specific configuration fields and save
them to the database
This commit is contained in:
parent
95baec99dd
commit
ce0f560c44
@ -7,17 +7,34 @@
|
||||
class StackForm extends Component
|
||||
{
|
||||
public $service;
|
||||
public $isConfigurationRequired = false;
|
||||
public $fields = [];
|
||||
protected $listeners = ["saveCompose"];
|
||||
protected $rules = [
|
||||
public $rules = [
|
||||
'service.docker_compose_raw' => 'required',
|
||||
'service.docker_compose' => 'required',
|
||||
'service.name' => 'required',
|
||||
'service.description' => 'nullable',
|
||||
];
|
||||
public function mount () {
|
||||
if ($this->service->applications->filter(fn($app) => str($app->image)->contains('minio/minio'))->count() > 0) {
|
||||
$this->isConfigurationRequired = true;
|
||||
public $validationAttributes = [];
|
||||
public function mount()
|
||||
{
|
||||
$extraFields = $this->service->extraFields();
|
||||
foreach ($extraFields as $serviceName => $fields) {
|
||||
foreach ($fields as $fieldKey => $field) {
|
||||
$key = data_get($field, 'key');
|
||||
$value = data_get($field, 'value');
|
||||
$rules = data_get($field, 'rules');
|
||||
$isPassword = data_get($field, 'isPassword');
|
||||
$this->fields[$key] = [
|
||||
"serviceName" => $serviceName,
|
||||
"key" => $key,
|
||||
"name" => $fieldKey,
|
||||
"value" => $value,
|
||||
"isPassword" => $isPassword,
|
||||
];
|
||||
$this->rules["fields.$key.value"] = $rules;
|
||||
$this->validationAttributes["fields.$key.value"] = $fieldKey;
|
||||
}
|
||||
}
|
||||
}
|
||||
public function saveCompose($raw)
|
||||
@ -32,6 +49,7 @@ public function submit()
|
||||
try {
|
||||
$this->validate();
|
||||
$this->service->save();
|
||||
$this->service->saveExtraFields($this->fields);
|
||||
$this->service->parse();
|
||||
$this->service->refresh();
|
||||
$this->service->saveComposeConfigs();
|
||||
|
@ -45,7 +45,168 @@ public function type()
|
||||
{
|
||||
return 'service';
|
||||
}
|
||||
public function extraFields()
|
||||
{
|
||||
$fields = collect([]);
|
||||
$applications = $this->applications()->get();
|
||||
foreach ($applications as $application) {
|
||||
$image = str($application->image)->before(':')->value();
|
||||
switch ($image) {
|
||||
case str($image)->contains('minio'):
|
||||
$console_url = $this->environment_variables()->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first();
|
||||
$s3_api_url = $this->environment_variables()->where('key', 'MINIO_SERVER_URL')->first();
|
||||
$admin_user = $this->environment_variables()->where('key', 'SERVICE_USER_MINIO')->first();
|
||||
$admin_password = $this->environment_variables()->where('key', 'SERVICE_PASSWORD_MINIO')->first();
|
||||
$fields->put('MinIO', [
|
||||
'Console URL' => [
|
||||
'key' => data_get($console_url, 'key'),
|
||||
'value' => data_get($console_url, 'value'),
|
||||
'rules' => 'required|url',
|
||||
],
|
||||
'S3 API URL' => [
|
||||
'key' => data_get($s3_api_url, 'key'),
|
||||
'value' => data_get($s3_api_url, 'value'),
|
||||
'rules' => 'required|url',
|
||||
],
|
||||
'Admin User' => [
|
||||
'key' => data_get($admin_user, 'key'),
|
||||
'value' => data_get($admin_user, 'value'),
|
||||
'rules' => 'required',
|
||||
],
|
||||
'Admin Password' => [
|
||||
'key' => data_get($admin_password, 'key'),
|
||||
'value' => data_get($admin_password, 'value'),
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
],
|
||||
]);
|
||||
break;
|
||||
}
|
||||
}
|
||||
$databases = $this->databases()->get();
|
||||
|
||||
foreach ($databases as $database) {
|
||||
$image = str($database->image)->before(':')->value();
|
||||
switch ($image) {
|
||||
case str($image)->contains('postgres'):
|
||||
$userVariables = ['SERVICE_USER_POSTGRES', 'SERVICE_USER_POSTGRESQL'];
|
||||
$passwordVariables = ['SERVICE_PASSWORD_POSTGRES', 'SERVICE_PASSWORD_POSTGRESQL'];
|
||||
$dbNameVariables = ['POSTGRESQL_DATABASE', 'POSTGRES_DB'];
|
||||
$postgres_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
|
||||
$postgres_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();
|
||||
$postgres_db_name = $this->environment_variables()->whereIn('key', $dbNameVariables)->first();
|
||||
$fields->put('PostgreSQL', [
|
||||
'User' => [
|
||||
'key' => data_get($postgres_user, 'key'),
|
||||
'value' => data_get($postgres_user, 'value'),
|
||||
'rules' => 'required',
|
||||
],
|
||||
'Password' => [
|
||||
'key' => data_get($postgres_password, 'key'),
|
||||
'value' => data_get($postgres_password, 'value'),
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
],
|
||||
'Database Name' => [
|
||||
'key' => data_get($postgres_db_name, 'key'),
|
||||
'value' => data_get($postgres_db_name, 'value'),
|
||||
'rules' => 'required',
|
||||
],
|
||||
]);
|
||||
break;
|
||||
case str($image)->contains('mysql'):
|
||||
$userVariables = ['SERVICE_USER_MYSQL', 'SERVICE_USER_WORDPRESS'];
|
||||
$passwordVariables = ['SERVICE_PASSWORD_MYSQL', 'SERVICE_PASSWORD_WORDPRESS'];
|
||||
$rootPasswordVariables = ['SERVICE_PASSWORD_MYSQLROOT', 'SERVICE_PASSWORD_ROOT'];
|
||||
$dbNameVariables = ['MYSQL_DATABASE'];
|
||||
$mysql_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
|
||||
$mysql_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();
|
||||
$mysql_root_password = $this->environment_variables()->whereIn('key', $rootPasswordVariables)->first();
|
||||
$mysql_db_name = $this->environment_variables()->whereIn('key', $dbNameVariables)->first();
|
||||
$fields->put('MySQL', [
|
||||
'User' => [
|
||||
'key' => data_get($mysql_user, 'key'),
|
||||
'value' => data_get($mysql_user, 'value'),
|
||||
'rules' => 'required',
|
||||
],
|
||||
'Password' => [
|
||||
'key' => data_get($mysql_password, 'key'),
|
||||
'value' => data_get($mysql_password, 'value'),
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
],
|
||||
'Root Password' => [
|
||||
'key' => data_get($mysql_root_password, 'key'),
|
||||
'value' => data_get($mysql_root_password, 'value'),
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
],
|
||||
'Database Name' => [
|
||||
'key' => data_get($mysql_db_name, 'key'),
|
||||
'value' => data_get($mysql_db_name, 'value'),
|
||||
'rules' => 'required',
|
||||
],
|
||||
]);
|
||||
break;
|
||||
case str($image)->contains('mariadb'):
|
||||
$userVariables = ['SERVICE_USER_MARIADB', 'SERVICE_USER_WORDPRESS', '_APP_DB_USER'];
|
||||
$passwordVariables = ['SERVICE_PASSWORD_MARIADB', 'SERVICE_PASSWORD_WORDPRESS', '_APP_DB_PASS'];
|
||||
$rootPasswordVariables = ['SERVICE_PASSWORD_MARIADBROOT', 'SERVICE_PASSWORD_ROOT', '_APP_DB_ROOT_PASS'];
|
||||
$dbNameVariables = ['SERVICE_DATABASE_MARIADB', 'SERVICE_DATABASE_WORDPRESS', '_APP_DB_SCHEMA'];
|
||||
$mariadb_user = $this->environment_variables()->whereIn('key', $userVariables)->first();
|
||||
$mariadb_password = $this->environment_variables()->whereIn('key', $passwordVariables)->first();
|
||||
$mariadb_root_password = $this->environment_variables()->whereIn('key', $rootPasswordVariables)->first();
|
||||
$mariadb_db_name = $this->environment_variables()->whereIn('key', $dbNameVariables)->first();
|
||||
$fields->put('MariaDB', [
|
||||
'User' => [
|
||||
'key' => data_get($mariadb_user, 'key'),
|
||||
'value' => data_get($mariadb_user, 'value'),
|
||||
'rules' => 'required',
|
||||
],
|
||||
'Password' => [
|
||||
'key' => data_get($mariadb_password, 'key'),
|
||||
'value' => data_get($mariadb_password, 'value'),
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
],
|
||||
'Root Password' => [
|
||||
'key' => data_get($mariadb_root_password, 'key'),
|
||||
'value' => data_get($mariadb_root_password, 'value'),
|
||||
'rules' => 'required',
|
||||
'isPassword' => true,
|
||||
],
|
||||
'Database Name' => [
|
||||
'key' => data_get($mariadb_db_name, 'key'),
|
||||
'value' => data_get($mariadb_db_name, 'value'),
|
||||
'rules' => data_get($mariadb_db_name, 'value') && 'required',
|
||||
],
|
||||
]);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
return $fields;
|
||||
}
|
||||
public function saveExtraFields($fields)
|
||||
{
|
||||
foreach ($fields as $field) {
|
||||
$key = data_get($field, 'key');
|
||||
$value = data_get($field, 'value');
|
||||
$found = $this->environment_variables()->where('key', $key)->first();
|
||||
if ($found) {
|
||||
$found->value = $value;
|
||||
$found->save();
|
||||
} else {
|
||||
$this->environment_variables()->create([
|
||||
'key' => $key,
|
||||
'value' => $value,
|
||||
'is_build_time' => false,
|
||||
'service_id' => $this->id,
|
||||
'is_preview' => false,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
public function documentation()
|
||||
{
|
||||
$services = getServiceTemplates();
|
||||
@ -257,7 +418,7 @@ public function parse(bool $isNew = false): Collection
|
||||
}
|
||||
}
|
||||
$networks = collect();
|
||||
foreach ($serviceNetworks as $key =>$serviceNetwork) {
|
||||
foreach ($serviceNetworks as $key => $serviceNetwork) {
|
||||
if (gettype($serviceNetwork) === 'string') {
|
||||
// networks:
|
||||
// - appwrite
|
||||
@ -268,7 +429,7 @@ public function parse(bool $isNew = false): Collection
|
||||
// ipv4_address: 192.168.203.254
|
||||
// $networks->put($serviceNetwork, null);
|
||||
ray($key);
|
||||
$networks->put($key,$serviceNetwork);
|
||||
$networks->put($key, $serviceNetwork);
|
||||
}
|
||||
}
|
||||
foreach ($definedNetwork as $key => $network) {
|
||||
@ -564,12 +725,18 @@ public function parse(bool $isNew = false): Collection
|
||||
}
|
||||
|
||||
// Add labels to the service
|
||||
$fqdns = collect(data_get($savedService, 'fqdns'));
|
||||
$defaultLabels = defaultLabels($this->id, $containerName, type: 'service', subType: $isDatabase ? 'database' : 'application', subId: $savedService->id);
|
||||
$serviceLabels = $serviceLabels->merge($defaultLabels);
|
||||
if (!$isDatabase && $fqdns->count() > 0) {
|
||||
if ($fqdns) {
|
||||
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($this->uuid, $fqdns, true));
|
||||
if (!$isDatabase) {
|
||||
if ($savedService->serviceType()) {
|
||||
$fqdns = generateServiceSpecificFqdns($savedService, forTraefik: true);
|
||||
} else {
|
||||
$fqdns = collect(data_get($savedService, 'fqdns'));
|
||||
}
|
||||
$defaultLabels = defaultLabels($this->id, $containerName, type: 'service', subType: $isDatabase ? 'database' : 'application', subId: $savedService->id);
|
||||
$serviceLabels = $serviceLabels->merge($defaultLabels);
|
||||
if ($fqdns->count() > 0) {
|
||||
if ($fqdns) {
|
||||
$serviceLabels = $serviceLabels->merge(fqdnLabelsForTraefik($this->uuid, $fqdns, true));
|
||||
}
|
||||
}
|
||||
}
|
||||
data_set($service, 'labels', $serviceLabels->toArray());
|
||||
|
@ -22,6 +22,16 @@ public function type()
|
||||
{
|
||||
return 'service';
|
||||
}
|
||||
public function serviceType()
|
||||
{
|
||||
$found = str(collect(SPECIFIC_SERVICES)->filter(function ($service) {
|
||||
return str($this->image)->before(':')->value() === $service;
|
||||
})->first());
|
||||
if ($found->isNotEmpty()) {
|
||||
return $found;
|
||||
}
|
||||
return null;
|
||||
}
|
||||
public function service()
|
||||
{
|
||||
return $this->belongsTo(Service::class);
|
||||
|
@ -29,7 +29,6 @@ public function databaseType()
|
||||
return "standalone-$image";
|
||||
}
|
||||
public function getServiceDatabaseUrl() {
|
||||
// $type = $this->databaseType();
|
||||
$port = $this->public_port;
|
||||
$realIp = $this->service->server->ip;
|
||||
if ($realIp === 'host.docker.internal' || isDev()) {
|
||||
|
@ -16,22 +16,28 @@ public function __construct(public Service $service)
|
||||
{
|
||||
$this->links = collect([]);
|
||||
$service->applications()->get()->map(function ($application) {
|
||||
if ($application->fqdn) {
|
||||
$fqdns = collect(Str::of($application->fqdn)->explode(','));
|
||||
$fqdns->map(function ($fqdn) {
|
||||
$this->links->push(getFqdnWithoutPort($fqdn));
|
||||
});
|
||||
}
|
||||
if ($application->ports) {
|
||||
$portsCollection = collect(Str::of($application->ports)->explode(','));
|
||||
$portsCollection->map(function ($port) {
|
||||
if (Str::of($port)->contains(':')) {
|
||||
$hostPort = Str::of($port)->before(':');
|
||||
} else {
|
||||
$hostPort = $port;
|
||||
}
|
||||
$this->links->push(base_url(withPort:false) . ":{$hostPort}");
|
||||
});
|
||||
$type = $application->serviceType();
|
||||
if ($type) {
|
||||
$links = generateServiceSpecificFqdns($application, false);
|
||||
$this->links = $this->links->merge($links);
|
||||
} else {
|
||||
if ($application->fqdn) {
|
||||
$fqdns = collect(Str::of($application->fqdn)->explode(','));
|
||||
$fqdns->map(function ($fqdn) {
|
||||
$this->links->push(getFqdnWithoutPort($fqdn));
|
||||
});
|
||||
}
|
||||
if ($application->ports) {
|
||||
$portsCollection = collect(Str::of($application->ports)->explode(','));
|
||||
$portsCollection->map(function ($port) {
|
||||
if (Str::of($port)->contains(':')) {
|
||||
$hostPort = Str::of($port)->before(':');
|
||||
} else {
|
||||
$hostPort = $port;
|
||||
}
|
||||
$this->links->push(base_url(withPort: false) . ":{$hostPort}");
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -23,3 +23,6 @@
|
||||
'influxdb',
|
||||
'clickhouse/clickhouse-server'
|
||||
];
|
||||
const SPECIFIC_SERVICES = [
|
||||
'quay.io/minio/minio',
|
||||
];
|
||||
|
@ -144,6 +144,39 @@ function defaultLabels($id, $name, $pull_request_id = 0, string $type = 'applica
|
||||
}
|
||||
return $labels;
|
||||
}
|
||||
function generateServiceSpecificFqdns($service, $forTraefik = false)
|
||||
{
|
||||
$variables = collect($service->service->environment_variables);
|
||||
$type = $service->serviceType();
|
||||
$payload = collect([]);
|
||||
switch ($type) {
|
||||
case $type->contains('minio'):
|
||||
$MINIO_BROWSER_REDIRECT_URL = $variables->where('key', 'MINIO_BROWSER_REDIRECT_URL')->first();
|
||||
if (is_null($MINIO_BROWSER_REDIRECT_URL->value)) {
|
||||
$MINIO_BROWSER_REDIRECT_URL->update([
|
||||
"value" => generateFqdn($service->service->server, 'console-' . $service->uuid)
|
||||
]);
|
||||
}
|
||||
$MINIO_SERVER_URL = $variables->where('key', 'MINIO_SERVER_URL')->first();
|
||||
if (is_null($MINIO_SERVER_URL->value)) {
|
||||
$MINIO_SERVER_URL->update([
|
||||
"value" => generateFqdn($service->service->server, 'minio-' . $service->uuid)
|
||||
]);
|
||||
}
|
||||
if ($forTraefik) {
|
||||
$payload = collect([
|
||||
$MINIO_BROWSER_REDIRECT_URL->value . ':9001',
|
||||
$MINIO_SERVER_URL->value . ':9000',
|
||||
]);
|
||||
} else {
|
||||
$payload = collect([
|
||||
$MINIO_BROWSER_REDIRECT_URL->value,
|
||||
$MINIO_SERVER_URL->value,
|
||||
]);
|
||||
}
|
||||
}
|
||||
return $payload;
|
||||
}
|
||||
function fqdnLabelsForTraefik(string $uuid, Collection $domains, bool $is_force_https_enabled, $onlyPort = null)
|
||||
{
|
||||
$labels = collect([]);
|
||||
|
@ -3,7 +3,7 @@
|
||||
href="{{ route('project.service', $parameters) }}">
|
||||
<button>Configuration</button>
|
||||
</a>
|
||||
<x-services.links :service="$service" />
|
||||
<x-services.links />
|
||||
<div class="flex-1"></div>
|
||||
@if (serviceStatus($service) === 'degraded')
|
||||
<button wire:click='deploy' onclick="startService.showModal()"
|
||||
|
@ -5,7 +5,7 @@
|
||||
<div class="fixed z-50 top-[4.5rem] left-4" id="vue">
|
||||
<magic-bar></magic-bar>
|
||||
</div>
|
||||
<main class="main max-w-screen-2xl">
|
||||
<main class="pb-10 main max-w-screen-2xl">
|
||||
{{ $slot }}
|
||||
</main>
|
||||
@endsection
|
||||
|
@ -16,19 +16,21 @@
|
||||
<x-forms.input label="Description" id="application.description"></x-forms.input>
|
||||
</div>
|
||||
<div class="flex gap-2">
|
||||
@if ($application->required_fqdn)
|
||||
<x-forms.input required placeholder="https://app.coolify.io" label="Domains"
|
||||
id="application.fqdn" helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io, https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input>
|
||||
@else
|
||||
<x-forms.input placeholder="https://app.coolify.io" label="Domains"
|
||||
id="application.fqdn" helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io, https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input>
|
||||
@if (!$application->serviceType()?->contains(str($application->image)->before(':')))
|
||||
@if ($application->required_fqdn)
|
||||
<x-forms.input required placeholder="https://app.coolify.io" label="Domains"
|
||||
id="application.fqdn"
|
||||
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io, https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input>
|
||||
@else
|
||||
<x-forms.input placeholder="https://app.coolify.io" label="Domains" id="application.fqdn"
|
||||
helper="You can specify one domain with path or more with comma. You can specify a port to bind the domain to.<br><br><span class='text-helper'>Example</span><br>- http://app.coolify.io, https://cloud.coolify.io/dashboard<br>- http://app.coolify.io/api/v3<br>- http://app.coolify.io:3000 -> app.coolify.io will point to port 3000 inside the container. "></x-forms.input>
|
||||
@endif
|
||||
@endif
|
||||
<x-forms.input required
|
||||
helper="You can change the image you would like to deploy.<br><br><span class='text-warning'>WARNING. You could corrupt your data. Only do it if you know what you are doing.</span>"
|
||||
label="Image" id="application.image"></x-forms.input>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 class="pt-2">Advanced</h3>
|
||||
<div class="w-64">
|
||||
<x-forms.checkbox instantSave label="Exclude from service status"
|
||||
|
@ -28,7 +28,7 @@
|
||||
<div class="w-full pl-8">
|
||||
<div x-cloak x-show="activeTab === 'service-stack'">
|
||||
<livewire:project.service.stack-form :service="$service" />
|
||||
<div class="grid grid-cols-1 gap-2 pt-4 xl:grid-cols-3">
|
||||
<div class="grid grid-cols-1 gap-2 pt-4 xl:grid-cols-1">
|
||||
@foreach ($applications as $application)
|
||||
<div @class([
|
||||
'border-l border-dashed border-red-500' => Str::of(
|
||||
@ -58,7 +58,7 @@
|
||||
@endif
|
||||
<div class="text-xs">{{ $application->status }}</div>
|
||||
</a>
|
||||
<a class="flex gap-2 p-1 mx-4 font-bold rounded group-hover:text-white hover:no-underline"
|
||||
<a class="flex items-center gap-2 p-1 mx-4 font-bold rounded group-hover:text-white hover:no-underline"
|
||||
href="{{ route('project.service.logs', [...$parameters, 'service_name' => $application->name]) }}"><span
|
||||
class="hover:text-warning">Logs</span></a>
|
||||
</div>
|
||||
@ -88,7 +88,7 @@ class="hover:text-warning">Logs</span></a>
|
||||
@endif
|
||||
<div class="text-xs">{{ $database->status }}</div>
|
||||
</a>
|
||||
<a class="flex gap-2 p-1 mx-4 font-bold rounded hover:no-underline group-hover:text-white"
|
||||
<a class="flex items-center gap-2 p-1 mx-4 font-bold rounded hover:no-underline group-hover:text-white"
|
||||
href="{{ route('project.service.logs', [...$parameters, 'service_name' => $database->name]) }}"><span
|
||||
class="hover:text-warning">Logs</span></a>
|
||||
</div>
|
||||
|
@ -12,9 +12,17 @@
|
||||
<x-forms.input id="service.name" required label="Service Name" placeholder="My super wordpress site" />
|
||||
<x-forms.input id="service.description" label="Description" />
|
||||
</div>
|
||||
{{-- @if ($isConfigurationRequired)
|
||||
<div class="text-warning">This service requires additional confiugration. Please check our <a
|
||||
href="https://coolify.io/docs" class="text-white underline">documentation</a> for further information.
|
||||
@if ($fields)
|
||||
<div>
|
||||
<h3>Service Specific Configuration</h3>
|
||||
</div>
|
||||
@endif --}}
|
||||
<div class="grid grid-cols-2 gap-2">
|
||||
@foreach ($fields as $serviceName => $fields)
|
||||
<x-forms.input type="{{ data_get($fields, 'isPassword') ? 'password' : 'text' }}" required
|
||||
helper="Variable name: {{ $serviceName }}"
|
||||
label="{{ data_get($fields, 'serviceName') }} {{ data_get($fields, 'name') }}"
|
||||
id="fields.{{ $serviceName }}.value"></x-forms.input>
|
||||
@endforeach
|
||||
</div>
|
||||
@endif
|
||||
</form>
|
||||
|
Loading…
Reference in New Issue
Block a user