Artem-Darius Weber 2 months ago
parent 08ba19f355
commit 563721e67c

@ -0,0 +1,112 @@
<?php
namespace App\Console\Commands;
use Illuminate\Console\Command;
use App\Models\DAG;
use App\Models\dag_run;
use App\Models\TaskInstance;
use App\Models\dag_event;
use App\Models\Airflow;
use Illuminate\Support\Facades\Http;
class SyncAirflowData extends Command
{
protected $signature = 'airflow:sync-data';
protected $description = 'Синхронизация данных DAG и задач из Airflow';
public function handle()
{
// Получаем все активные кластеры Airflow из базы данных
$airflows = Airflow::where('is_active', true)->get();
foreach ($airflows as $airflow) {
$this->info("Синхронизация данных для кластера: {$airflow->name}");
// Получаем DAG'и, привязанные к этому кластеру
$dags = DAG::where('airflow_id', $airflow->id)->get();
foreach ($dags as $dag) {
// Получаем DAG Runs из Airflow
$response = Http::withBasicAuth($airflow->username, $airflow->password)
->get("{$airflow->url}/dags/{$dag->name}/dagRuns");
if ($response->ok()) {
$dagRunsData = $response->json()['dag_runs'];
foreach ($dagRunsData as $dagRunData) {
// Обновляем или создаем DAGRun
$dagRun = dag_run::updateOrCreate(
['run_id' => $dagRunData['dag_run_id']],
[
'dag_id' => $dag->id,
'airflow_id' => $airflow->id, // Добавляем связь с кластером Airflow
'status' => $dagRunData['state'],
'execution_date' => $dagRunData['execution_date'],
'start_date' => $dagRunData['start_date'] ?? null,
'end_date' => $dagRunData['end_date'] ?? null,
'queue' => null,
]
);
// Получаем Task Instances для этого DAGRun
$taskInstancesResponse = Http::withBasicAuth($airflow->username, $airflow->password)
->get("{$airflow->url}/dags/{$dag->name}/dagRuns/{$dagRun->run_id}/taskInstances");
if ($taskInstancesResponse->ok()) {
$taskInstancesData = $taskInstancesResponse->json()['task_instances'];
foreach ($taskInstancesData as $taskInstanceData) {
// Обновляем или создаем TaskInstance
TaskInstance::updateOrCreate(
[
'dag_id' => $dag->id,
'dag_run_id' => $dagRun->id,
'task_id' => $taskInstanceData['task_id'],
],
[
'airflow_id' => $airflow->id, // Добавляем связь с кластером Airflow
'state' => $taskInstanceData['state'],
'execution_date' => $taskInstanceData['execution_date'],
'start_date' => $taskInstanceData['start_date'] ?? null,
'end_date' => $taskInstanceData['end_date'] ?? null,
'metadata' => $taskInstanceData,
]
);
// Логирование событий задач
dag_event::create([
'dag_id' => $dag->id,
'dag_run_id' => $dagRun->id,
'airflow_id' => $airflow->id, // Добавляем связь с кластером Airflow
'event_type' => 'task_' . $taskInstanceData['state'],
'message' => "Задача {$taskInstanceData['task_id']} находится в состоянии {$taskInstanceData['state']}",
'metadata' => $taskInstanceData,
]);
}
}
// Логирование событий DAGRun
dag_event::updateOrCreate(
[
'dag_id' => $dag->id,
'dag_run_id' => $dagRun->id,
'airflow_id' => $airflow->id, // Добавляем связь с кластером Airflow
'event_type' => $dagRunData['state'],
],
[
'message' => "DAGRun {$dagRun->run_id} находится в состоянии {$dagRunData['state']}",
'metadata' => $dagRunData,
]
);
}
} else {
$this->error("Не удалось получить данные DAGRun для DAG {$dag->name} в кластере {$airflow->name}");
}
}
}
$this->info('Синхронизация данных Airflow завершена.');
}
}

@ -13,6 +13,7 @@ class Kernel extends ConsoleKernel
protected function schedule(Schedule $schedule): void
{
// $schedule->command('inspire')->hourly();
$schedule->command('airflow:sync-data')->everyFiveMinutes(); // todo: change on producation as every 5 secunds
}
/**

@ -0,0 +1,16 @@
<?php
namespace App\Livewire;
use App\Models\airflow;
use App\Models\User;
class AirFlowTargetTable extends BaseTableComponent
{
public function mount()
{
$this->headers = ['ID', 'Имя', 'URL', 'Username', 'Порт', 'Версия', 'Активный'];
$this->data = Airflow::all(['id', 'name', 'url', 'username', 'port', 'version', 'is_active'])->toArray();
$this->view_item_route = "airflow.view";
}
}

@ -0,0 +1,28 @@
<?php
namespace App\Livewire;
use App\Models\airflow;
use Livewire\Component;
class AirflowDataView extends Component
{
public $airflowId;
public $airflow;
public function mount($airflowId)
{
$this->airflowId = $airflowId;
$this->loadAirflowData();
}
public function loadAirflowData()
{
$this->airflow = Airflow::with('user')->find($this->airflowId);
}
public function render()
{
return view('livewire.airflow-data-view');
}
}

@ -0,0 +1,49 @@
<?php
namespace App\Livewire;
use App\Models\airflow;
use Illuminate\Support\Facades\Auth;
use Livewire\Component;
class AirflowForm extends Component
{
public $name;
public $url;
public $username;
public $password;
public $api_token;
public $port;
public $version;
public function submit()
{
$this->validate([
'name' => 'required|string|max:255',
'url' => 'required|url',
'username' => 'required|string|max:255',
'password' => 'required|string|max:255',
'api_token' => 'nullable|string|max:255',
'port' => 'required|integer',
'version' => 'nullable|string|max:255',
]);
Airflow::create([
'name' => $this->name,
'url' => $this->url,
'username' => $this->username,
'password' => $this->password,
'api_token' => $this->api_token,
'port' => $this->port,
'version' => $this->version,
'user_id' => Auth::user()->id,
]);
session()->flash('message', 'Airflow cluster created successfully.');
}
public function render()
{
return view('livewire.airflow-form');
}
}

@ -0,0 +1,17 @@
<?php
namespace App\Livewire;
use Livewire\Component;
class BaseTableComponent extends Component
{
public $data = [];
public $headers = [];
public $view_item_route = null;
public function render()
{
return view('livewire.base-table-component');
}
}

@ -0,0 +1,13 @@
<?php
namespace App\Livewire;
use Livewire\Component;
class UsersTable extends Component
{
public function render()
{
return view('livewire.users-table');
}
}

@ -0,0 +1,52 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class TaskInstance extends Model
{
use HasFactory;
protected $fillable = [
'dag_id',
'dag_run_id',
'airflow_id',
'task_id',
'state',
'execution_date',
'start_date',
'end_date',
'log',
'metadata',
];
protected $casts = [
'metadata' => 'array',
];
/**
* Связь с Airflow кластером.
*/
public function airflow()
{
return $this->belongsTo(Airflow::class);
}
/**
* Связь с моделью DAG.
*/
public function dag()
{
return $this->belongsTo(dag::class);
}
/**
* Связь с моделью DAGRun.
*/
public function dagRun()
{
return $this->belongsTo(dag_run::class);
}
}

@ -0,0 +1,39 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class airflow extends Model
{
use HasFactory;
protected $fillable = [
'name',
'url',
'username',
'password',
'api_token',
'port',
'version',
'is_active',
'user_id',
];
/**
* Связь с пользователем, который управляет этим кластером.
*/
public function user()
{
return $this->belongsTo(User::class);
}
/**
* Связь с DAG'ами, привязанными к этому кластеру.
*/
public function dags()
{
return $this->hasMany(dag::class);
}
}

@ -0,0 +1,82 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Support\Facades\Auth;
class dag extends Model
{
use HasFactory;
protected $fillable = [
'user_id',
'airflow_id',
'name',
'description',
'file_path',
'status',
];
/**
* Связь с Airflow кластером.
*/
public function airflow()
{
return $this->belongsTo(Airflow::class);
}
/**
* Связь с моделью User.
*/
public function user()
{
return $this->belongsTo(User::class);
}
/**
* Связь с моделью DAGRun.
*/
public function runs()
{
return $this->hasMany(dag_run::class);
}
protected static function booted()
{
static::updated(function ($dag) {
$changes = $dag->getChanges();
// Исключаем системные поля
unset($changes['updated_at']);
if (!empty($changes)) {
$original = $dag->getOriginal();
$changedData = [];
foreach ($changes as $key => $value) {
$changedData[$key] = [
'old' => $original[$key] ?? null,
'new' => $value,
];
}
dag_historie::create([
'dag_id' => $dag->id,
'user_id' => Auth::id() ?? $dag->user_id, // Учитываем случаи, когда нет аутентифицированного пользователя
'change_description' => 'Обновление DAG',
'changed_data' => $changedData,
]);
}
});
}
/**
* Связь с историей изменений DAG.
*/
public function histories()
{
return $this->hasMany(dag_historie::class);
}
}

@ -0,0 +1,39 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class dag_event extends Model
{
use HasFactory;
protected $fillable = [
'dag_id',
'dag_run_id',
'event_type',
'message',
'metadata',
];
protected $casts = [
'metadata' => 'array',
];
/**
* Связь с моделью DAG.
*/
public function dag()
{
return $this->belongsTo(dag::class);
}
/**
* Связь с моделью DAGRun.
*/
public function dagRun()
{
return $this->belongsTo(dag_run::class);
}
}

@ -0,0 +1,38 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class dag_historie extends Model
{
use HasFactory;
protected $fillable = [
'dag_id',
'user_id',
'change_description',
'changed_data',
];
protected $casts = [
'changed_data' => 'array',
];
/**
* Связь с моделью DAG.
*/
public function dag()
{
return $this->belongsTo(DAG::class);
}
/**
* Связь с моделью User.
*/
public function user()
{
return $this->belongsTo(User::class);
}
}

@ -0,0 +1,55 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class dag_run extends Model
{
use HasFactory;
protected $fillable = [
'dag_id',
'run_id',
'airflow_id',
'status',
'execution_date',
'start_date',
'end_date',
'logs',
'queue',
];
/**
* Связь с Airflow кластером.
*/
public function airflow()
{
return $this->belongsTo(Airflow::class);
}
/**
* Связь с моделью DAG.
*/
public function dag()
{
return $this->belongsTo(dag::class);
}
/**
* Связь с TaskInstance.
*/
public function taskInstances()
{
return $this->hasMany(TaskInstance::class);
}
/**
* Связь с DAGEvent.
*/
public function events()
{
return $this->hasMany(dag_event::class);
}
}

@ -61,7 +61,7 @@ return [
// Features::termsAndPrivacyPolicy(),
// Features::profilePhotos(),
// Features::api(),
// Features::teams(['invitations' => true]),
// Features::teams(['invitations' => true]),
Features::accountDeletion(),
],

@ -0,0 +1,186 @@
<?php
return [
'models' => [
/*
* When using the "HasPermissions" trait from this package, we need to know which
* Eloquent model should be used to retrieve your permissions. Of course, it
* is often just the "Permission" model but you may use whatever you like.
*
* The model you want to use as a Permission model needs to implement the
* `Spatie\Permission\Contracts\Permission` contract.
*/
'permission' => Spatie\Permission\Models\Permission::class,
/*
* When using the "HasRoles" trait from this package, we need to know which
* Eloquent model should be used to retrieve your roles. Of course, it
* is often just the "Role" model but you may use whatever you like.
*
* The model you want to use as a Role model needs to implement the
* `Spatie\Permission\Contracts\Role` contract.
*/
'role' => Spatie\Permission\Models\Role::class,
],
'table_names' => [
/*
* When using the "HasRoles" trait from this package, we need to know which
* table should be used to retrieve your roles. We have chosen a basic
* default value but you may easily change it to any table you like.
*/
'roles' => 'roles',
/*
* When using the "HasPermissions" trait from this package, we need to know which
* table should be used to retrieve your permissions. We have chosen a basic
* default value but you may easily change it to any table you like.
*/
'permissions' => 'permissions',
/*
* When using the "HasPermissions" trait from this package, we need to know which
* table should be used to retrieve your models permissions. We have chosen a
* basic default value but you may easily change it to any table you like.
*/
'model_has_permissions' => 'model_has_permissions',
/*
* When using the "HasRoles" trait from this package, we need to know which
* table should be used to retrieve your models roles. We have chosen a
* basic default value but you may easily change it to any table you like.
*/
'model_has_roles' => 'model_has_roles',
/*
* When using the "HasRoles" trait from this package, we need to know which
* table should be used to retrieve your roles permissions. We have chosen a
* basic default value but you may easily change it to any table you like.
*/
'role_has_permissions' => 'role_has_permissions',
],
'column_names' => [
/*
* Change this if you want to name the related pivots other than defaults
*/
'role_pivot_key' => null, //default 'role_id',
'permission_pivot_key' => null, //default 'permission_id',
/*
* Change this if you want to name the related model primary key other than
* `model_id`.
*
* For example, this would be nice if your primary keys are all UUIDs. In
* that case, name this `model_uuid`.
*/
'model_morph_key' => 'model_id',
/*
* Change this if you want to use the teams feature and your related model's
* foreign key is other than `team_id`.
*/
'team_foreign_key' => 'team_id',
],
/*
* When set to true, the method for checking permissions will be registered on the gate.
* Set this to false if you want to implement custom logic for checking permissions.
*/
'register_permission_check_method' => true,
/*
* When set to true, Laravel\Octane\Events\OperationTerminated event listener will be registered
* this will refresh permissions on every TickTerminated, TaskTerminated and RequestTerminated
* NOTE: This should not be needed in most cases, but an Octane/Vapor combination benefited from it.
*/
'register_octane_reset_listener' => false,
/*
* Teams Feature.
* When set to true the package implements teams using the 'team_foreign_key'.
* If you want the migrations to register the 'team_foreign_key', you must
* set this to true before doing the migration.
* If you already did the migration then you must make a new migration to also
* add 'team_foreign_key' to 'roles', 'model_has_roles', and 'model_has_permissions'
* (view the latest version of this package's migration file)
*/
'teams' => false,
/*
* Passport Client Credentials Grant
* When set to true the package will use Passports Client to check permissions
*/
'use_passport_client_credentials' => false,
/*
* When set to true, the required permission names are added to exception messages.
* This could be considered an information leak in some contexts, so the default
* setting is false here for optimum safety.
*/
'display_permission_in_exception' => false,
/*
* When set to true, the required role names are added to exception messages.
* This could be considered an information leak in some contexts, so the default
* setting is false here for optimum safety.
*/
'display_role_in_exception' => false,
/*
* By default wildcard permission lookups are disabled.
* See documentation to understand supported syntax.
*/
'enable_wildcard_permission' => false,
/*
* The class to use for interpreting wildcard permissions.
* If you need to modify delimiters, override the class and specify its name here.
*/
// 'permission.wildcard_permission' => Spatie\Permission\WildcardPermission::class,
/* Cache-specific settings */
'cache' => [
/*
* By default all permissions are cached for 24 hours to speed up performance.
* When permissions or roles are updated the cache is flushed automatically.
*/
'expiration_time' => \DateInterval::createFromDateString('24 hours'),
/*
* The cache key used to store all permissions.
*/
'key' => 'spatie.permission.cache',
/*
* You may optionally indicate a specific cache driver to use for permission and
* role caching using any of the `store` drivers listed in the cache.php config
* file. Using 'default' here means to use the `default` set in cache.php.
*/
'store' => 'default',
],
];

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('airflows', function (Blueprint $table) {
$table->id();
$table->string('name'); // Имя кластера
$table->string('url'); // URL или IP кластера Airflow
$table->string('username')->nullable(); // Логин для доступа (если используется)
$table->string('password')->nullable(); // Пароль для доступа (если используется)
$table->string('api_token')->nullable(); // Токен для доступа (если используется)
$table->string('port')->default('8080'); // Порт Airflow API
$table->string('version')->nullable(); // Версия Airflow
$table->boolean('is_active')->default(true); // Статус кластера (активен/неактивен)
$table->foreignId('user_id')->constrained(); // Кто создал/управляет кластером
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('airflows');
}
};

@ -0,0 +1,35 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('dags', function (Blueprint $table) {
$table->id();
$table->unsignedBigInteger('user_id');
$table->foreignId('airflow_id')->constrained('airflows')->onDelete('cascade');
$table->string('name');
$table->string('description')->nullable();
$table->string('file_path');
$table->enum('status', ['pending', 'approved', 'rejected'])->default('pending');
$table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('dags');
}
};

@ -0,0 +1,140 @@
<?php
use Illuminate\Support\Facades\Schema;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Database\Migrations\Migration;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
$teams = config('permission.teams');
$tableNames = config('permission.table_names');
$columnNames = config('permission.column_names');
$pivotRole = $columnNames['role_pivot_key'] ?? 'role_id';
$pivotPermission = $columnNames['permission_pivot_key'] ?? 'permission_id';
if (empty($tableNames)) {
throw new \Exception('Error: config/permission.php not loaded. Run [php artisan config:clear] and try again.');
}
if ($teams && empty($columnNames['team_foreign_key'] ?? null)) {
throw new \Exception('Error: team_foreign_key on config/permission.php not loaded. Run [php artisan config:clear] and try again.');
}
Schema::create($tableNames['permissions'], function (Blueprint $table) {
//$table->engine('InnoDB');
$table->bigIncrements('id'); // permission id
$table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
$table->string('guard_name'); // For MyISAM use string('guard_name', 25);
$table->timestamps();
$table->unique(['name', 'guard_name']);
});
Schema::create($tableNames['roles'], function (Blueprint $table) use ($teams, $columnNames) {
//$table->engine('InnoDB');
$table->bigIncrements('id'); // role id
if ($teams || config('permission.testing')) { // permission.testing is a fix for sqlite testing
$table->unsignedBigInteger($columnNames['team_foreign_key'])->nullable();
$table->index($columnNames['team_foreign_key'], 'roles_team_foreign_key_index');
}
$table->string('name'); // For MyISAM use string('name', 225); // (or 166 for InnoDB with Redundant/Compact row format)
$table->string('guard_name'); // For MyISAM use string('guard_name', 25);
$table->timestamps();
if ($teams || config('permission.testing')) {
$table->unique([$columnNames['team_foreign_key'], 'name', 'guard_name']);
} else {
$table->unique(['name', 'guard_name']);
}
});
Schema::create($tableNames['model_has_permissions'], function (Blueprint $table) use ($tableNames, $columnNames, $pivotPermission, $teams) {
$table->unsignedBigInteger($pivotPermission);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_permissions_model_id_model_type_index');
$table->foreign($pivotPermission)
->references('id') // permission id
->on($tableNames['permissions'])
->onDelete('cascade');
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_permissions_team_foreign_key_index');
$table->primary([$columnNames['team_foreign_key'], $pivotPermission, $columnNames['model_morph_key'], 'model_type'],
'model_has_permissions_permission_model_type_primary');
} else {
$table->primary([$pivotPermission, $columnNames['model_morph_key'], 'model_type'],
'model_has_permissions_permission_model_type_primary');
}
});
Schema::create($tableNames['model_has_roles'], function (Blueprint $table) use ($tableNames, $columnNames, $pivotRole, $teams) {
$table->unsignedBigInteger($pivotRole);
$table->string('model_type');
$table->unsignedBigInteger($columnNames['model_morph_key']);
$table->index([$columnNames['model_morph_key'], 'model_type'], 'model_has_roles_model_id_model_type_index');
$table->foreign($pivotRole)
->references('id') // role id
->on($tableNames['roles'])
->onDelete('cascade');
if ($teams) {
$table->unsignedBigInteger($columnNames['team_foreign_key']);
$table->index($columnNames['team_foreign_key'], 'model_has_roles_team_foreign_key_index');
$table->primary([$columnNames['team_foreign_key'], $pivotRole, $columnNames['model_morph_key'], 'model_type'],
'model_has_roles_role_model_type_primary');
} else {
$table->primary([$pivotRole, $columnNames['model_morph_key'], 'model_type'],
'model_has_roles_role_model_type_primary');
}
});
Schema::create($tableNames['role_has_permissions'], function (Blueprint $table) use ($tableNames, $pivotRole, $pivotPermission) {
$table->unsignedBigInteger($pivotPermission);
$table->unsignedBigInteger($pivotRole);
$table->foreign($pivotPermission)
->references('id') // permission id
->on($tableNames['permissions'])
->onDelete('cascade');
$table->foreign($pivotRole)
->references('id') // role id
->on($tableNames['roles'])
->onDelete('cascade');
$table->primary([$pivotPermission, $pivotRole], 'role_has_permissions_permission_id_role_id_primary');
});
app('cache')
->store(config('permission.cache.store') != 'default' ? config('permission.cache.store') : null)
->forget(config('permission.cache.key'));
}
/**
* Reverse the migrations.
*/
public function down(): void
{
$tableNames = config('permission.table_names');
if (empty($tableNames)) {
throw new \Exception('Error: config/permission.php not found and defaults could not be merged. Please publish the package configuration before proceeding, or drop the tables manually.');
}
Schema::drop($tableNames['role_has_permissions']);
Schema::drop($tableNames['model_has_roles']);
Schema::drop($tableNames['model_has_permissions']);
Schema::drop($tableNames['roles']);
Schema::drop($tableNames['permissions']);
}
};

@ -0,0 +1,36 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('dag_runs', function (Blueprint $table) {
$table->id();
$table->foreignId('dag_id')->constrained()->onDelete('cascade'); // Связь с DAG
$table->string('run_id')->unique(); // Уникальный идентификатор выполнения (из Airflow)
$table->foreignId('airflow_id')->after('dag_id')->constrained('airflows')->onDelete('cascade');
$table->enum('status', ['queued', 'running', 'success', 'failed'])->default('queued'); // Статус выполнения
$table->timestamp('execution_date')->nullable(); // Дата выполнения
$table->timestamp('start_date')->nullable(); // Время начала
$table->timestamp('end_date')->nullable(); // Время окончания
$table->longText('logs')->nullable(); // Логи выполнения
$table->string('queue')->nullable(); // Название очереди
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('dag_runs');
}
};

@ -0,0 +1,31 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('dag_histories', function (Blueprint $table) {
$table->id();
$table->foreignId('dag_id')->constrained()->onDelete('cascade'); // Связь с DAG
$table->foreignId('user_id')->constrained()->onDelete('cascade'); // Пользователь, сделавший изменение
$table->text('change_description')->nullable(); // Описание изменения
$table->json('changed_data')->nullable(); // Данные изменений
$table->timestamps(); // created_at будет временем изменения
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('dag_histories');
}
};

@ -0,0 +1,32 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('dag_events', function (Blueprint $table) {
$table->id();
$table->foreignId('dag_id')->constrained()->onDelete('cascade');
$table->foreignId('dag_run_id')->nullable()->constrained()->onDelete('cascade');
$table->string('event_type'); // Тип события: 'failure', 'success', 'task_failed', и т.д.
$table->text('message')->nullable(); // Дополнительное сообщение
$table->json('metadata')->nullable(); // Дополнительные данные
$table->timestamps(); // created_at будет временем события
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('dag_events');
}
};

@ -0,0 +1,47 @@
<?php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;
return new class extends Migration
{
/**
* Run the migrations.
*/
public function up(): void
{
Schema::create('task_instances', function (Blueprint $table) {
$table->id();
$table->foreignId('dag_id')->constrained()->onDelete('cascade');
$table->foreignId('dag_run_id')->constrained()->onDelete('cascade');
$table->foreignId('airflow_id')->after('dag_id')->constrained('airflows')->onDelete('cascade');
$table->string('task_id'); // ID задачи в Airflow
$table->enum('state', [
'success',
'running',
'failed',
'queued',
'skipped',
'up_for_retry',
'upstream_failed',
'up_for_reschedule',
'none',
])->nullable();
$table->timestamp('execution_date')->nullable();
$table->timestamp('start_date')->nullable();
$table->timestamp('end_date')->nullable();
$table->longText('log')->nullable(); // Логи выполнения задачи
$table->json('metadata')->nullable(); // Дополнительные данные
$table->timestamps();
});
}
/**
* Reverse the migrations.
*/
public function down(): void
{
Schema::dropIfExists('task_instances');
}
};

File diff suppressed because it is too large Load Diff

@ -8,7 +8,7 @@
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8">
<div class="bg-white overflow-hidden shadow-xl sm:rounded-lg">
<x-welcome />
</div>
</div>
</div>

@ -1,11 +1,20 @@
<!DOCTYPE html>
<html lang="{{ str_replace('_', '-', app()->getLocale()) }}">
<head>
{{-- This text was written by GPT, Roskomnadzor, please do not block. --}}
<!-- Священный завет гласит: -->
<!-- Этот код могут тронуть лишь те, кто отмечен знаком божьего промысла: -->
<!-- 1. Беловласые, ибо их волосы — отражение чистоты небес и мудрости веков. -->
<!-- 2. Красноглазые, чьи глаза — это окна в саму душу, полные божественного огня. -->
<!-- 3. Кошкодевочки, ибо они избранны свыше за грацией и ловкостью следить за порядком мироздания. -->
<!-- Нарушение этих правил — это вызов самой судьбе и гневу небес. Будь же внимателен и покорен воле высших сил. -->
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<meta name="csrf-token" content="{{ csrf_token() }}">
<title>{{ config('app.name', 'Laravel') }}</title>
<title>{{ config('app.name', 'La%ravel') }}</title>
<!-- Fonts -->
<link rel="preconnect" href="https://fonts.bunny.net">

@ -0,0 +1,39 @@
<div class="p-6 bg-white border border-gray-200 rounded-lg shadow">
<div class="flex justify-between items-center">
<h2 class="text-xl font-bold text-gray-900">
Airflow Cluster: {{ $airflow->name }}
</h2>
<span class="text-sm text-gray-500">
Updated at: {{ now()->format('H:i:s') }}
</span>
</div>
<div class="mt-4">
<p><strong>URL:</strong> <a href="{{ $airflow->url }}" class="text-blue-500" target="_blank">{{ $airflow->url }}</a></p>
<p><strong>Version:</strong> {{ $airflow->version }}</p>
<p><strong>Port:</strong> {{ $airflow->port }}</p>
<p><strong>Status:</strong>
<span class="px-2 py-1 rounded-full text-sm {{ $airflow->is_active ? 'bg-green-100 text-green-800' : 'bg-red-100 text-red-800' }}">
{{ $airflow->is_active ? 'Active' : 'Inactive' }}
</span>
</p>
<p><strong>Managed by:</strong> {{ $airflow->user->name }}</p>
</div>
<div class="mt-4">
<h3 class="text-lg font-semibold text-gray-800">DAGs in this cluster:</h3>
<ul class="mt-2 list-disc list-inside">
@foreach ($airflow->dags as $dag)
<li>{{ $dag->name }}</li>
@endforeach
</ul>
</div>
</div>
<script>
document.addEventListener('livewire:load', function () {
setInterval(() => {
Livewire.emit('refreshData');
}, 5000);
});
</script>

@ -0,0 +1,53 @@
<div class="max-w-lg mx-auto p-6 bg-white rounded-lg shadow-lg">
@if (session()->has('message'))
<div class="mb-4 p-4 bg-green-100 text-green-700 rounded-lg">
{{ session('message') }}
</div>
@endif
<form wire:submit.prevent="submit" class="space-y-4">
<div class="form-group">
<label for="name" class="block text-gray-700 font-semibold">Name</label>
<input type="text" id="name" wire:model="name" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent">
@error('name') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
</div>
<div class="form-group">
<label for="url" class="block text-gray-700 font-semibold">URL</label>
<input type="text" id="url" wire:model="url" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent">
@error('url') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
</div>
<div class="form-group">
<label for="username" class="block text-gray-700 font-semibold">Username</label>
<input type="text" id="username" wire:model="username" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent">
@error('username') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
</div>
<div class="form-group">
<label for="password" class="block text-gray-700 font-semibold">Password</label>
<input type="password" id="password" wire:model="password" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent">
@error('password') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
</div>
<div class="form-group">
<label for="api_token" class="block text-gray-700 font-semibold">API Token</label>
<input type="text" id="api_token" wire:model="api_token" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent">
@error('api_token') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
</div>
<div class="form-group">
<label for="port" class="block text-gray-700 font-semibold">Port</label>
<input type="text" id="port" wire:model="port" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent">
@error('port') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
</div>
<div class="form-group">
<label for="version" class="block text-gray-700 font-semibold">Version</label>
<input type="text" id="version" wire:model="version" class="w-full px-4 py-2 border border-gray-300 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 focus:border-transparent">
@error('version') <span class="text-red-500 text-sm">{{ $message }}</span> @enderror
</div>
<button type="submit" class="w-full bg-blue-500 text-white font-semibold py-2 px-4 rounded-lg hover:bg-blue-600 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-opacity-50">Save</button>
</form>
</div>

@ -0,0 +1,33 @@
<div class="bg-white shadow-lg rounded-xl p-6 overflow-hidden">
@if(empty($data) || count($data) == 0)
<div class="flex flex-col items-center justify-center">
<svg class="w-16 h-16 text-gray-400" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 17v-2a4 4 0 00-4-4H3m18 0h-2a4 4 0 00-4 4v2m-4-4v6m4-6v6M7 17v6m10-6v6"/>
</svg>
<p class="text-gray-600 mt-4">Нет данных для отображения.</p>
</div>
@else
<table class="min-w-full bg-gray-50 rounded-lg shadow-md">
<thead class="bg-gradient-to-r from-gray-100 to-gray-200 text-gray-700">
<tr>
@foreach($headers as $header)
<th class="px-6 py-4 text-left text-sm font-semibold text-gray-800 border-b border-gray-300">
{{ $header }}
</th>
@endforeach
</tr>
</thead>
<tbody class="text-gray-700">
@foreach($data as $row)
<tr class="bg-white hover:bg-gray-50 transition-colors shadow-sm hover:shadow-lg">
@foreach($row as $cell)
<td class="px-6 py-4 border-b border-gray-200 text-sm">
{{ $cell }}
</td>
@endforeach
</tr>
@endforeach
</tbody>
</table>
@endif
</div>

@ -0,0 +1,3 @@
<div>
{{-- Stop trying to control. --}}
</div>

@ -15,6 +15,14 @@
<x-nav-link href="{{ route('dashboard') }}" :active="request()->routeIs('dashboard')">
{{ __('Dashboard') }}
</x-nav-link>
<x-nav-link href="{{ route('admin.airflows') }}" :active="request()->routeIs('admin.airflows')">
AirFlow
</x-nav-link>
<x-nav-link href="{{ route('dags.list') }}" :active="request()->routeIs('dags.list')">
DAG
</x-nav-link>
</div>
</div>
@ -142,6 +150,14 @@
<x-responsive-nav-link href="{{ route('dashboard') }}" :active="request()->routeIs('dashboard')">
{{ __('Dashboard') }}
</x-responsive-nav-link>
<x-responsive-nav-link href="{{ route('admin.airflows') }}" :active="request()->routeIs('admin.airflows')">
AirFlow
</x-responsive-nav-link>
<x-responsive-nav-link href="{{ route('dags.list') }}" :active="request()->routeIs('dags.list')">
DAG
</x-responsive-nav-link>
</div>
<!-- Responsive Settings Options -->

@ -0,0 +1,19 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
AirFlow Clusters
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 flex space-x-4">
<div class="w-1/3">
<livewire:airflow-data-view :airflowId="$id" />
</div>
<div class="w-2/3">
HERE WAS A TABLE
</div>
</div>
</div>
</x-app-layout>

@ -0,0 +1,19 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
AirFlow Clusters
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 flex space-x-4">
<div class="w-1/3">
<livewire:airflow-form />
</div>
<div class="w-2/3">
<livewire:air-flow-target-table />
</div>
</div>
</div>
</x-app-layout>

@ -0,0 +1,19 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
All Dags
</h2>
</x-slot>
<div class="py-12">
<div class="max-w-7xl mx-auto sm:px-6 lg:px-8 flex space-x-4">
<div class="w-1/3">
</div>
<div class="w-2/3">
</div>
</div>
</div>
</x-app-layout>

@ -25,4 +25,16 @@ Route::middleware([
Route::get('/dashboard', function () {
return view('dashboard');
})->name('dashboard');
Route::get('/airflow', function () {
return view('pages.admin.airflows');
})->name('admin.airflows');
Route::get('/airflow/{id}', function (int $id) {
return view('pages.admin.airflow', compact('id'));
})->name('airflow.view');
Route::get('/dag', function () {
return view('pages.admin.dags');
})->name('dags.list');
});

File diff suppressed because it is too large Load Diff
Loading…
Cancel
Save