diff --git a/app/Console/Commands/SyncAirflowData.php b/app/Console/Commands/SyncAirflowData.php
new file mode 100644
index 0000000..9392ef1
--- /dev/null
+++ b/app/Console/Commands/SyncAirflowData.php
@@ -0,0 +1,112 @@
+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 завершена.');
+ }
+}
diff --git a/app/Console/Kernel.php b/app/Console/Kernel.php
index e6b9960..0661ca8 100644
--- a/app/Console/Kernel.php
+++ b/app/Console/Kernel.php
@@ -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
}
/**
diff --git a/app/Livewire/AirFlowTargetTable.php b/app/Livewire/AirFlowTargetTable.php
new file mode 100644
index 0000000..9000345
--- /dev/null
+++ b/app/Livewire/AirFlowTargetTable.php
@@ -0,0 +1,16 @@
+headers = ['ID', 'Имя', 'URL', 'Username', 'Порт', 'Версия', 'Активный'];
+ $this->data = Airflow::all(['id', 'name', 'url', 'username', 'port', 'version', 'is_active'])->toArray();
+ $this->view_item_route = "airflow.view";
+ }
+}
diff --git a/app/Livewire/AirflowDataView.php b/app/Livewire/AirflowDataView.php
new file mode 100644
index 0000000..9252437
--- /dev/null
+++ b/app/Livewire/AirflowDataView.php
@@ -0,0 +1,28 @@
+airflowId = $airflowId;
+ $this->loadAirflowData();
+ }
+
+ public function loadAirflowData()
+ {
+ $this->airflow = Airflow::with('user')->find($this->airflowId);
+ }
+
+ public function render()
+ {
+ return view('livewire.airflow-data-view');
+ }
+}
diff --git a/app/Livewire/AirflowForm.php b/app/Livewire/AirflowForm.php
new file mode 100644
index 0000000..8a5e81c
--- /dev/null
+++ b/app/Livewire/AirflowForm.php
@@ -0,0 +1,49 @@
+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');
+ }
+}
diff --git a/app/Livewire/BaseTableComponent.php b/app/Livewire/BaseTableComponent.php
new file mode 100644
index 0000000..68046b4
--- /dev/null
+++ b/app/Livewire/BaseTableComponent.php
@@ -0,0 +1,17 @@
+ '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);
+ }
+}
diff --git a/app/Models/airflow.php b/app/Models/airflow.php
new file mode 100644
index 0000000..6a09326
--- /dev/null
+++ b/app/Models/airflow.php
@@ -0,0 +1,39 @@
+belongsTo(User::class);
+ }
+
+ /**
+ * Связь с DAG'ами, привязанными к этому кластеру.
+ */
+ public function dags()
+ {
+ return $this->hasMany(dag::class);
+ }
+}
diff --git a/app/Models/dag.php b/app/Models/dag.php
new file mode 100644
index 0000000..80114c0
--- /dev/null
+++ b/app/Models/dag.php
@@ -0,0 +1,82 @@
+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);
+ }
+}
diff --git a/app/Models/dag_event.php b/app/Models/dag_event.php
new file mode 100644
index 0000000..60d70c0
--- /dev/null
+++ b/app/Models/dag_event.php
@@ -0,0 +1,39 @@
+ 'array',
+ ];
+
+ /**
+ * Связь с моделью DAG.
+ */
+ public function dag()
+ {
+ return $this->belongsTo(dag::class);
+ }
+
+ /**
+ * Связь с моделью DAGRun.
+ */
+ public function dagRun()
+ {
+ return $this->belongsTo(dag_run::class);
+ }
+}
diff --git a/app/Models/dag_historie.php b/app/Models/dag_historie.php
new file mode 100644
index 0000000..a193a64
--- /dev/null
+++ b/app/Models/dag_historie.php
@@ -0,0 +1,38 @@
+ 'array',
+ ];
+
+ /**
+ * Связь с моделью DAG.
+ */
+ public function dag()
+ {
+ return $this->belongsTo(DAG::class);
+ }
+
+ /**
+ * Связь с моделью User.
+ */
+ public function user()
+ {
+ return $this->belongsTo(User::class);
+ }
+}
diff --git a/app/Models/dag_run.php b/app/Models/dag_run.php
new file mode 100644
index 0000000..0e5bc99
--- /dev/null
+++ b/app/Models/dag_run.php
@@ -0,0 +1,55 @@
+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);
+ }
+}
diff --git a/config/jetstream.php b/config/jetstream.php
index d5e5f11..ea287a5 100644
--- a/config/jetstream.php
+++ b/config/jetstream.php
@@ -61,7 +61,7 @@ return [
// Features::termsAndPrivacyPolicy(),
// Features::profilePhotos(),
// Features::api(),
- // Features::teams(['invitations' => true]),
+// Features::teams(['invitations' => true]),
Features::accountDeletion(),
],
diff --git a/config/permission.php b/config/permission.php
new file mode 100644
index 0000000..2a520f3
--- /dev/null
+++ b/config/permission.php
@@ -0,0 +1,186 @@
+ [
+
+ /*
+ * 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',
+ ],
+];
diff --git a/database/migrations/2024_10_01_211759_create_airflows_table.php b/database/migrations/2024_10_01_211759_create_airflows_table.php
new file mode 100644
index 0000000..0d41e87
--- /dev/null
+++ b/database/migrations/2024_10_01_211759_create_airflows_table.php
@@ -0,0 +1,36 @@
+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');
+ }
+};
diff --git a/database/migrations/2024_10_01_224955_create_dags_table.php b/database/migrations/2024_10_01_224955_create_dags_table.php
new file mode 100644
index 0000000..0cc94b0
--- /dev/null
+++ b/database/migrations/2024_10_01_224955_create_dags_table.php
@@ -0,0 +1,35 @@
+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');
+ }
+};
diff --git a/database/migrations/2024_10_01_225847_create_permission_tables.php b/database/migrations/2024_10_01_225847_create_permission_tables.php
new file mode 100644
index 0000000..9c7044b
--- /dev/null
+++ b/database/migrations/2024_10_01_225847_create_permission_tables.php
@@ -0,0 +1,140 @@
+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']);
+ }
+};
diff --git a/database/migrations/2024_10_01_232237_create_dag_runs_table.php b/database/migrations/2024_10_01_232237_create_dag_runs_table.php
new file mode 100644
index 0000000..181e440
--- /dev/null
+++ b/database/migrations/2024_10_01_232237_create_dag_runs_table.php
@@ -0,0 +1,36 @@
+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');
+ }
+};
diff --git a/database/migrations/2024_10_01_232313_create_dag_histories_table.php b/database/migrations/2024_10_01_232313_create_dag_histories_table.php
new file mode 100644
index 0000000..03598f4
--- /dev/null
+++ b/database/migrations/2024_10_01_232313_create_dag_histories_table.php
@@ -0,0 +1,31 @@
+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');
+ }
+};
diff --git a/database/migrations/2024_10_01_232612_create_dag_events_table.php b/database/migrations/2024_10_01_232612_create_dag_events_table.php
new file mode 100644
index 0000000..f235332
--- /dev/null
+++ b/database/migrations/2024_10_01_232612_create_dag_events_table.php
@@ -0,0 +1,32 @@
+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');
+ }
+};
diff --git a/database/migrations/2024_10_01_232653_create_task_instances_table.php b/database/migrations/2024_10_01_232653_create_task_instances_table.php
new file mode 100644
index 0000000..b5e692a
--- /dev/null
+++ b/database/migrations/2024_10_01_232653_create_task_instances_table.php
@@ -0,0 +1,47 @@
+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');
+ }
+};
diff --git a/final-instructions.md b/final-instructions.md
new file mode 100644
index 0000000..b03d487
--- /dev/null
+++ b/final-instructions.md
@@ -0,0 +1,1163 @@
+у меня есть AirFlow класстер и github репозиторий для хранения DAGs. Я хочу написать Laravel Jetstream Livewire App для учета и загрузки DAGs в репозиторий чтобы AirFlow мог получить к ним доступ. Никто из пользователей не имеет доступ к Github репозиторию. Доступ осуществляется только через Laravel App в рамках предоставленных прав пользователя. Также я хотел бы видеть статусы выполнения и очередь пользовательского загруженного DAG в Laravel App. Обобщи следующие предложения и напиши финальный пошаговый план для реализации моей задачи:
+
+### **1. Настройка Laravel Jetstream с Livewire**
+
+- **Установка Laravel:**
+
+ ```bash
+ composer create-project laravel/laravel airflow-dag-manager
+ ```
+
+- **Установка Jetstream с Livewire:**
+
+ ```bash
+ composer require laravel/jetstream
+ php artisan jetstream:install livewire
+ npm install
+ npm run dev
+ php artisan migrate
+ ```
+
+---
+
+### **2. Реализация аутентификации и авторизации пользователей**
+
+- **Аутентификация:**
+
+ Jetstream предоставляет готовые механизмы регистрации, входа и восстановления пароля.
+
+- **Авторизация:**
+
+ - Определите роли и разрешения пользователей.
+ - Используйте **Policies** или **Gates** Laravel для контроля доступа.
+ - Рекомендуется использовать пакет [Spatie Laravel-Permission](https://github.com/spatie/laravel-permission):
+
+ ```bash
+ composer require spatie/laravel-permission
+ ```
+
+ - Настройте роли (например, `admin`, `user`) и разрешения (`upload dag`, `view status`).
+
+---
+
+### **3. Интеграция с GitHub API для загрузки DAG-файлов**
+
+- **Аутентификация GitHub API:**
+
+ - Создайте **Personal Access Token** с необходимыми правами (`repo` для приватных репозиториев).
+ - Сохраните токен в файле `.env`:
+
+ ```
+ GITHUB_TOKEN=ваш_токен
+ ```
+
+- **Использование GitHub API:**
+
+ - Используйте пакет [knplabs/github-api](https://github.com/KnpLabs/php-github-api) или **Guzzle** для взаимодействия с API.
+
+ ```bash
+ composer require knplabs/github-api
+ ```
+
+ - Пример загрузки файла в репозиторий:
+
+ ```php
+ use Github\Client;
+
+ $client = new Client();
+ $client->authenticate(env('GITHUB_TOKEN'), null, Client::AUTH_ACCESS_TOKEN);
+
+ $username = 'ваш_логин';
+ $repository = 'имя_репозитория';
+ $branch = 'main';
+ $path = 'dags/' . $dagFilename;
+ $content = file_get_contents($uploadedDagFile);
+
+ // Получение текущего SHA коммита
+ $reference = $client->api('gitData')->references()->show($username, $repository, 'heads/' . $branch);
+ $commitSha = $reference['object']['sha'];
+
+ // Создание нового блоба
+ $blob = $client->api('gitData')->blobs()->create($username, $repository, [
+ 'content' => base64_encode($content),
+ 'encoding' => 'base64',
+ ]);
+
+ // Создание нового дерева
+ $tree = $client->api('gitData')->trees()->create($username, $repository, [
+ 'base_tree' => $commitSha,
+ 'tree' => [
+ [
+ 'path' => $path,
+ 'mode' => '100644',
+ 'type' => 'blob',
+ 'sha' => $blob['sha'],
+ ],
+ ],
+ ]);
+
+ // Создание нового коммита
+ $commit = $client->api('gitData')->commits()->create($username, $repository, [
+ 'message' => 'Добавлен DAG ' . $dagFilename,
+ 'tree' => $tree['sha'],
+ 'parents' => [$commitSha],
+ ]);
+
+ // Обновление ссылки
+ $client->api('gitData')->references()->update($username, $repository, 'heads/' . $branch, [
+ 'sha' => $commit['sha'],
+ ]);
+ ```
+
+- **Отслеживание загруженных DAG:**
+
+- Создайте таблицу `dags` в базе данных с полями `id`, `user_id`, `filename`, `created_at` и т.д.
+
+### **5. Получение статусов выполнения и очереди из Airflow**
+
+- **API Airflow:**
+
+- Включите REST API в `airflow.cfg`:
+
+ ```
+ [api]
+ auth_backend = airflow.api.auth.backend.basic_auth
+ ```
+
+- **Аутентификация:**
+
+ - Используйте Basic Auth или другой поддерживаемый метод.
+
+- **Получение статусов DAG:**
+
+ ```php
+ use Illuminate\Support\Facades\Http;
+
+ $response = Http::withBasicAuth('airflow_username', 'airflow_password')
+ ->get('http://airflow-server:8080/api/v1/dags/{dag_id}/dagRuns');
+
+ $dagRuns = $response->json();
+ ```
+
+1. Компонент для загрузки DAG-файлов
+1.1. Создание компонента
+Создайте Livewire-компонент UploadDagComponent:
+
+```bash
+php artisan make:livewire UploadDagComponent
+```
+
+1.2. Код компонента
+app/Http/Livewire/UploadDagComponent.php
+
+```php
+ 'required|file|mimes:py|max:1024', // Максимальный размер 1MB
+ ];
+
+ public function uploadDag()
+ {
+ $this->validate();
+
+ // Сохранение файла временно
+ $path = $this->dagFile->store('temp_dags');
+
+ $filename = $this->dagFile->getClientOriginalName();
+ $content = Storage::get($path);
+
+ // Интеграция с GitHub API
+ $client = new Client();
+ $client->authenticate(env('GITHUB_TOKEN'), null, Client::AUTH_ACCESS_TOKEN);
+
+ $username = env('GITHUB_USERNAME');
+ $repository = env('GITHUB_REPOSITORY');
+ $branch = 'main';
+ $dagPath = 'dags/' . $filename;
+
+ // Получение SHA последнего коммита
+ $reference = $client->api('gitData')->references()->show($username, $repository, 'heads/' . $branch);
+ $commitSha = $reference['object']['sha'];
+
+ // Создание нового блоба
+ $blob = $client->api('gitData')->blobs()->create($username, $repository, [
+ 'content' => base64_encode($content),
+ 'encoding' => 'base64',
+ ]);
+
+ // Создание нового дерева
+ $tree = $client->api('gitData')->trees()->create($username, $repository, [
+ 'base_tree' => $commitSha,
+ 'tree' => [
+ [
+ 'path' => $dagPath,
+ 'mode' => '100644',
+ 'type' => 'blob',
+ 'sha' => $blob['sha'],
+ ],
+ ],
+ ]);
+
+ // Создание нового коммита
+ $commit = $client->api('gitData')->commits()->create($username, $repository, [
+ 'message' => 'Добавлен DAG ' . $filename,
+ 'tree' => $tree['sha'],
+ 'parents' => [$commitSha],
+ ]);
+
+ // Обновление ссылки
+ $client->api('gitData')->references()->update($username, $repository, 'heads/' . $branch, [
+ 'sha' => $commit['sha'],
+ ]);
+
+ // Удаление временного файла
+ Storage::delete($path);
+
+ // Сохранение информации о DAG в базе данных
+ Auth::user()->dags()->create([
+ 'filename' => $filename,
+ 'commit_sha' => $commit['sha'],
+ ]);
+
+ session()->flash('message', 'DAG успешно загружен и отправлен в репозиторий GitHub.');
+
+ // Обновление компонента списка DAG
+ $this->emit('dagUploaded');
+ }
+
+ public function render()
+ {
+ return view('livewire.upload-dag-component');
+ }
+}
+```
+1.3. Шаблон компонента
+resources/views/livewire/upload-dag-component.blade.php
+
+```php
+
+ @if (session()->has('message'))
+
+ {{ session('message') }}
+
+ @endif
+
+
+
+```
+1.4. Обновление модели пользователя
+Добавьте связь между пользователем и DAG в модели User.
+
+app/Models/User.php
+
+```php
+public function dags()
+{
+ return $this->hasMany(Dag::class);
+}
+```
+2. Компонент для отображения списка DAG-файлов
+2.1. Создание компонента
+Создайте Livewire-компонент DagListComponent:
+
+```bash
+php artisan make:livewire DagListComponent
+```
+
+2.2. Код компонента
+app/Http/Livewire/DagListComponent.php
+
+```php
+ 'render'];
+
+ public function render()
+ {
+ $dags = Auth::user()->dags()->latest()->get();
+
+ return view('livewire.dag-list-component', [
+ 'dags' => $dags,
+ ]);
+ }
+}
+```
+
+2.3. Шаблон компонента
+resources/views/livewire/dag-list-component.blade.php
+
+```php
+
+@endsection
+```
+
+5. Модель DAG
+5.1. Создание модели и миграции
+Создайте модель Dag с миграцией:
+
+```bash
+php artisan make:model Dag -m
+```
+
+5.2. Обновление миграции
+database/migrations/xxxx_xx_xx_create_dags_table.php
+
+```php
+public function up()
+{
+ Schema::create('dags', function (Blueprint $table) {
+ $table->id();
+ $table->unsignedBigInteger('user_id');
+ $table->string('filename');
+ $table->string('commit_sha');
+ $table->timestamps();
+
+ $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
+ });
+}
+```
+
+5.3. Обновление модели DAG
+app/Models/Dag.php
+
+```php
+belongsTo(User::class);
+ }
+}
+```
+
+5.4. Миграция базы данных
+
+```bash
+php artisan migrate
+```
+
+6. Настройка переменных окружения
+Добавьте следующие переменные в файл .env:
+
+```env
+GITHUB_TOKEN=ваш_токен
+GITHUB_USERNAME=ваш_логин
+GITHUB_REPOSITORY=имя_репозитория
+AIRFLOW_API_URL=http://airflow-server:8080
+AIRFLOW_USERNAME=airflow_username
+AIRFLOW_PASSWORD=airflow_password
+```
+
+7. Добавление авторизации (опционально)
+Для контроля доступа к компонентам и функциям используйте встроенные возможности Laravel или пакет Spatie Laravel-Permission.
+
+
+8.3. Уведомления
+Реализуйте систему уведомлений при изменении статусов DAG (например, через Laravel Notifications).
+
+1. Компонент UploadDagComponent
+Описание:
+
+Этот компонент отвечает за загрузку DAG-файлов пользователями и их сохранение в GitHub репозитории через GitHub API.
+
+Основные функции:
+
+Форма для загрузки файлов.
+Валидация загружаемых файлов.
+Обработка загрузки и отправка файла в GitHub репозиторий.
+Сохранение информации о DAG в базе данных.
+Реализация:
+
+Создание компонента:
+
+```bash
+php artisan make:livewire UploadDagComponent
+```
+
+Свойства компонента:
+
+$file – для хранения загружаемого файла.
+$dagName – имя DAG (если требуется).
+Методы компонента:
+
+rules() – для определения правил валидации:
+
+```php
+protected $rules = [
+ 'file' => 'required|file|mimes:py|max:1024', // Максимум 1MB
+];
+```
+
+uploadDag() – основной метод для обработки загрузки:
+
+
+Выполнить валидацию файла.
+Прочитать содержимое файла.
+Использовать GitHub API для загрузки файла в репозиторий (как описано в вашем основном коде).
+Сохранить информацию о DAG в базе данных, связав его с текущим пользователем (auth()->user()->id).
+Шаблон компонента (Blade):
+
+Форма с полем для выбора файла:
+
+```php
+
+```
+
+Отображение сообщений об успехе или ошибках.
+
+2. Компонент DagListComponent
+Описание:
+
+Этот компонент отображает список DAG-файлов, загруженных текущим пользователем.
+
+Основные функции:
+
+Вывод списка DAG с информацией о каждом файле.
+Возможность удаления или обновления DAG (опционально).
+Пагинация, если количество DAG большое.
+Реализация:
+
+Создание компонента:
+
+bash
+Копировать код
+php artisan make:livewire DagListComponent
+Свойства компонента:
+
+$dags – коллекция DAG-файлов пользователя.
+Методы компонента:
+
+mount() или render() – для загрузки списка DAG:
+
+```php
+public function render()
+{
+ $this->dags = Dag::where('user_id', auth()->id())->get();
+ return view('livewire.dag-list-component');
+}
+```
+
+deleteDag($dagId) – метод для удаления DAG (если необходимо).
+
+Шаблон компонента (Blade):
+
+Таблица или список для отображения DAG:
+
+```php
+
+
+
+
Имя файла
+
Дата загрузки
+
Действия
+
+
+
+ @foreach($dags as $dag)
+
+
{{ $dag->filename }}
+
{{ $dag->created_at }}
+
+
+
+
+ @endforeach
+
+
+```
+
+3. Компонент DagStatusComponent
+Описание:
+
+Отображает текущий статус выполнения DAG, получая данные из Airflow API.
+
+Основные функции:
+
+Получение статусов DAG из Airflow API.
+Обновление статусов в реальном времени.
+Отображение статусов в удобном формате.
+Реализация:
+
+Создание компонента:
+
+bash
+Копировать код
+php artisan make:livewire DagStatusComponent
+Свойства компонента:
+
+$dagStatuses – массив статусов DAG.
+Методы компонента:
+
+mount() – для инициализации данных.
+
+getDagStatuses() – метод для обращения к Airflow API:
+
+```php
+public function getDagStatuses()
+{
+ $dags = Dag::where('user_id', auth()->id())->get();
+ foreach ($dags as $dag) {
+ $response = Http::withBasicAuth('airflow_username', 'airflow_password')
+ ->get("http://airflow-server:8080/api/v1/dags/{$dag->dag_id}/dagRuns");
+ if ($response->successful()) {
+ $this->dagStatuses[$dag->dag_id] = $response->json();
+ }
+ }
+}
+```
+
+Вы можете использовать wire:poll для автоматического обновления статусов:
+
+```php
+
+```
+
+4. Компонент DagQueueComponent
+Описание:
+
+Отображает очередь задач DAG для текущего пользователя, используя данные из Airflow API.
+
+Основные функции:
+
+Получение информации о запланированных и выполняемых задачах.
+Обновление данных в реальном времени.
+Реализация:
+
+Создание компонента:
+
+bash
+Копировать код
+php artisan make:livewire DagQueueComponent
+Свойства компонента:
+
+$dagQueue – массив задач в очереди.
+Методы компонента:
+
+getDagQueue() – обращение к Airflow API для получения очереди задач:
+
+```php
+public function getDagQueue()
+{
+ $response = Http::withBasicAuth('airflow_username', 'airflow_password')
+ ->get('http://airflow-server:8080/api/v1/queues');
+ if ($response->successful()) {
+ $this->dagQueue = $response->json();
+ }
+}
+```
+
+Используйте wire:poll для обновления данных:
+
+```php
+
+ @foreach($dagQueue as $task)
+ @if(in_array($task['dag_id'], $userDagIds))
+
Задача {{ $task['task_id'] }} в DAG {{ $task['dag_id'] }}
+ @endif
+ @endforeach
+
+```
+
+Здесь $userDagIds – массив dag_id текущего пользователя.
+
+Общие рекомендации для компонентов
+Валидация и безопасность:
+
+Всегда проверяйте права доступа, чтобы пользователи могли взаимодействовать только со своими данными.
+Используйте методы authorize() или проверки внутри методов компонентов.
+Обработка ошибок:
+
+Обрабатывайте возможные исключения при работе с внешними API.
+Предоставляйте пользователям понятные сообщения об ошибках.
+Пользовательский интерфейс:
+
+Делайте интерфейс интуитивно понятным и отзывчивым.
+Используйте стандартные компоненты UI или фреймворки CSS, такие как Tailwind CSS (который идет с Jetstream).
+Интеграция компонентов в приложение
+Маршруты и контроллеры:
+
+Определите маршруты в web.php для страниц, где будут использоваться компоненты.
+
+```php
+Route::middleware(['auth:sanctum', 'verified'])->get('/dashboard', function () {
+ return view('dashboard');
+})->name('dashboard');
+```
+
+Шаблоны Blade:
+
+Вставьте компоненты в соответствующие шаблоны.
+
+```php
+
+@extends('layouts.app')
+
+@section('content')
+
+
+
+
+
+
+@endsection
+```
+
+Дополнительные настройки
+Конфигурация Airflow API:
+
+Убедитесь, что Airflow API доступен и настроен для приема запросов от вашего приложения.
+
+Добавьте необходимые переменные в .env файл для хранения URL и данных аутентификации:
+
+```env
+AIRFLOW_API_URL=http://airflow-server:8080/api/v1
+AIRFLOW_API_USER=airflow_username
+AIRFLOW_API_PASSWORD=airflow_password
+```
+
+Конфигурация GitHub API:
+
+Обновите конфигурацию GitHub API, если требуется, и храните токен в .env файле:
+
+```env
+GITHUB_TOKEN=ваш_токен
+GITHUB_USERNAME=ваш_логин
+GITHUB_REPOSITORY=имя_репозитория
+GITHUB_BRANCH=main
+```
+
+Модели и миграции:
+
+Создайте модель Dag и соответствующую миграцию:
+
+bash
+Копировать код
+php artisan make:model Dag -m
+Поля таблицы dags могут включать:
+
+id
+user_id
+dag_id или filename
+created_at
+updated_at
+Не забудьте установить связь между моделями User и Dag.
+
+```php
+// В модели User
+public function dags()
+{
+ return $this->hasMany(Dag::class);
+}
+
+// В модели Dag
+public function user()
+{
+ return $this->belongsTo(User::class);
+}
+```
+
+Заключение
+Следуя этим инструкциям, вы сможете создать полноценное приложение на Laravel Jetstream Livewire, которое позволит пользователям:
+
+Загружать DAG-файлы без прямого доступа к GitHub репозиторию.
+Просматривать список своих DAG-файлов.
+Отслеживать статусы выполнения и очередь задач своих DAG в Airflow.
+Не забудьте протестировать каждую часть приложения и убедиться, что все компоненты работают корректно вместе.
+
+Извиняюсь за недочет. Сейчас я опишу более конкретные шаги разработки приложения с финальным исходным кодом.
+
+### Полный пошаговый план разработки приложения с исходным кодом:
+
+---
+
+### **Шаг 1: Установка Laravel и Jetstream с Livewire**
+
+1. **Создание нового проекта Laravel:**
+ ```bash
+ composer create-project laravel/laravel airflow-dag-manager
+ ```
+
+2. **Установка Jetstream с Livewire:**
+ ```bash
+ composer require laravel/jetstream
+ php artisan jetstream:install livewire
+ npm install
+ npm run dev
+ php artisan migrate
+ ```
+
+3. **Настройка аутентификации:**
+ Jetstream автоматически добавит базовые функции аутентификации.
+
+---
+
+### **Шаг 2: Настройка ролей и прав доступа**
+
+1. Установка пакета Spatie Laravel-Permission для управления ролями и правами доступа:
+ ```bash
+ composer require spatie/laravel-permission
+ ```
+
+2. Публикация конфигурации и выполнение миграций:
+ ```bash
+ php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
+ php artisan migrate
+ ```
+
+3. **Настройка ролей и прав:**
+ Добавьте роли, например, `admin` и `user`, а также права, такие как `upload dag`, `view status`. Пример настройки ролей можно добавить в seeder:
+
+ ```php
+ use Spatie\Permission\Models\Role;
+ use Spatie\Permission\Models\Permission;
+
+ // В seeder:
+ Role::create(['name' => 'admin']);
+ Role::create(['name' => 'user']);
+
+ Permission::create(['name' => 'upload dag']);
+ Permission::create(['name' => 'view status']);
+ ```
+
+---
+
+### **Шаг 3: Интеграция с GitHub API**
+
+1. **Создание GitHub Personal Access Token:**
+ Перейдите в GitHub, создайте новый токен с правами `repo` для доступа к репозиторию. Сохраните его в `.env`:
+ ```env
+ GITHUB_TOKEN=ваш_токен
+ GITHUB_USERNAME=ваш_логин
+ GITHUB_REPOSITORY=имя_репозитория
+ GITHUB_BRANCH=main
+ ```
+
+2. **Установка библиотеки для работы с GitHub API:**
+ ```bash
+ composer require knplabs/github-api
+ ```
+
+3. **Создание метода для загрузки DAG в репозиторий:**
+
+ Создайте компонент **UploadDagComponent** для загрузки DAG:
+
+ ```bash
+ php artisan make:livewire UploadDagComponent
+ ```
+
+ **Код компонента UploadDagComponent:**
+ ```php
+ 'required|file|mimes:py|max:1024',
+ ];
+
+ public function uploadDag()
+ {
+ $this->validate();
+
+ // Сохранение временного файла
+ $path = $this->dagFile->store('temp_dags');
+
+ // Получение содержимого файла
+ $filename = $this->dagFile->getClientOriginalName();
+ $content = Storage::get($path);
+
+ // Интеграция с GitHub API
+ $client = new Client();
+ $client->authenticate(env('GITHUB_TOKEN'), null, Client::AUTH_ACCESS_TOKEN);
+
+ $username = env('GITHUB_USERNAME');
+ $repository = env('GITHUB_REPOSITORY');
+ $branch = env('GITHUB_BRANCH');
+ $dagPath = 'dags/' . $filename;
+
+ // Получение SHA последнего коммита
+ $reference = $client->api('gitData')->references()->show($username, $repository, 'heads/' . $branch);
+ $commitSha = $reference['object']['sha'];
+
+ // Создание нового блоба
+ $blob = $client->api('gitData')->blobs()->create($username, $repository, [
+ 'content' => base64_encode($content),
+ 'encoding' => 'base64',
+ ]);
+
+ // Создание нового дерева
+ $tree = $client->api('gitData')->trees()->create($username, $repository, [
+ 'base_tree' => $commitSha,
+ 'tree' => [
+ [
+ 'path' => $dagPath,
+ 'mode' => '100644',
+ 'type' => 'blob',
+ 'sha' => $blob['sha'],
+ ],
+ ],
+ ]);
+
+ // Создание нового коммита
+ $commit = $client->api('gitData')->commits()->create($username, $repository, [
+ 'message' => 'Добавлен DAG ' . $filename,
+ 'tree' => $tree['sha'],
+ 'parents' => [$commitSha],
+ ]);
+
+ // Обновление ссылки
+ $client->api('gitData')->references()->update($username, $repository, 'heads/' . $branch, [
+ 'sha' => $commit['sha'],
+ ]);
+
+ // Удаление временного файла
+ Storage::delete($path);
+
+ // Сообщение об успехе
+ session()->flash('message', 'DAG успешно загружен и отправлен в репозиторий GitHub.');
+ }
+
+ public function render()
+ {
+ return view('livewire.upload-dag-component');
+ }
+ }
+ ```
+
+4. **Шаблон компонента (Blade):**
+ ```blade
+
+ @if (session()->has('message'))
+
+ {{ session('message') }}
+
+ @endif
+
+
+
+ ```
+
+---
+
+### **Шаг 4: Синхронизация DAG с Airflow**
+
+1. **Настройка Airflow для получения DAG:**
+ Вы можете настроить Airflow для автоматического выполнения `git pull` либо использовать GitHub Actions для деплоя DAG на сервер Airflow.
+
+---
+
+### **Шаг 5: Получение статусов выполнения DAG через Airflow API**
+
+1. **Включение Airflow API:**
+ В `airflow.cfg` добавьте или активируйте следующие строки:
+ ```ini
+ [api]
+ auth_backend = airflow.api.auth.backend.basic_auth
+ ```
+
+2. **Запросы к Airflow API для получения статусов DAG:**
+
+ Создайте компонент **DagStatusComponent**:
+
+ ```bash
+ php artisan make:livewire DagStatusComponent
+ ```
+
+ **Код компонента DagStatusComponent:**
+ ```php
+ fetchDagStatuses();
+ }
+
+ public function fetchDagStatuses()
+ {
+ $response = Http::withBasicAuth(env('AIRFLOW_USERNAME'), env('AIRFLOW_PASSWORD'))
+ ->get(env('AIRFLOW_API_URL') . '/api/v1/dags/{dag_id}/dagRuns');
+
+ if ($response->ok()) {
+ $this->dagRuns = $response->json();
+ }
+ }
+
+ public function render()
+ {
+ return view('livewire.dag-status-component');
+ }
+ }
+ ```
+
+3. **Шаблон компонента (Blade):**
+ ```blade
+
+@endsection
+5. Модель DAG
+5.1. Создание модели и миграции
+Создайте модель Dag с миграцией:
+
+bash
+Копировать код
+php artisan make:model Dag -m
+5.2. Обновление миграции
+database/migrations/xxxx_xx_xx_create_dags_table.php
+
+php
+Копировать код
+public function up()
+{
+ Schema::create('dags', function (Blueprint $table) {
+ $table->id();
+ $table->unsignedBigInteger('user_id');
+ $table->string('filename');
+ $table->string('commit_sha');
+ $table->timestamps();
+
+ $table->foreign('user_id')->references('id')->on('users')->onDelete('cascade');
+ });
+}
+5.3. Обновление модели DAG
+app/Models/Dag.php
+
+php
+Копировать код
+belongsTo(User::class);
+ }
+}
+5.4. Миграция базы данных
+bash
+Копировать код
+php artisan migrate
+6. Настройка переменных окружения
+Добавьте следующие переменные в файл .env:
+
+makefile
+Копировать код
+GITHUB_TOKEN=ваш_токен
+GITHUB_USERNAME=ваш_логин
+GITHUB_REPOSITORY=имя_репозитория
+AIRFLOW_API_URL=http://airflow-server:8080
+AIRFLOW_USERNAME=airflow_username
+AIRFLOW_PASSWORD=airflow_password
+7. Добавление авторизации (опционально)
+Для контроля доступа к компонентам и функциям используйте встроенные возможности Laravel или пакет Spatie Laravel-Permission.
+
+7.1. Установка пакета
+bash
+Копировать код
+composer require spatie/laravel-permission
+7.2. Публикация и миграция
+bash
+Копировать код
+php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
+php artisan migrate
+7.3. Настройка ролей и разрешений
+В контроллере или сервисе установите роли и разрешения для пользователей.
+
+8. Дополнительные рекомендации
+8.1. Обработка ошибок
+Добавьте обработку ошибок при взаимодействии с GitHub и Airflow API.
+Используйте блоки try-catch и отображайте информативные сообщения пользователям.
+8.2. Улучшение интерфейса
+Используйте стилизацию и компоненты из Tailwind CSS или Bootstrap для улучшения UI.
+Добавьте подтверждения перед действиями, если необходимо.
+8.3. Уведомления
+Реализуйте систему уведомлений при изменении статусов DAG (например, через Laravel Notifications).
+9. Заключение
+Теперь у вас есть необходимые Livewire-компоненты для реализации требуемых функций в вашем приложении Laravel. Эти компоненты позволяют пользователям загружать DAG-файлы, просматривать свои загруженные DAG, а также отслеживать статусы выполнения и очереди задач из Airflow.
+
+Важно: Убедитесь, что вы заменили все переменные окружения на реальные значения и протестировали взаимодействие с GitHub и Airflow API в вашей среде.
+
+Полезные ссылки
+Livewire Documentation: https://laravel-livewire.com/docs/2.x/quickstart
+GitHub API for PHP: https://github.com/KnpLabs/php-github-api
+Airflow REST API Reference: https://airflow.apache.org/docs/apache-airflow/stable/stable-rest-api-ref.html
+Laravel Authentication: https://laravel.com/docs/8.x/authentication
+Spatie Laravel-Permission: https://spatie.be/docs/laravel-permission/v5/introduction
+При необходимости вы можете расширять функциональность приложения, добавляя новые компоненты и улучшая существующие. Если у вас возникнут дополнительные вопросы или потребуется помощь, обращайтесь. Удачи в разработке!
+
+Для обеспечения требуемых функций вам понадобятся следующие Livewire компоненты:
+
+UploadDagComponent – для загрузки DAG-файлов пользователями.
+DagListComponent – для отображения списка загруженных пользователем DAG-файлов.
+DagStatusComponent – для отображения статусов выполнения DAG.
+DagQueueComponent – для отображения очереди задач DAG.
+Я подробно опишу каждый компонент, его функциональность и основные методы, учитывая ваш основной код, без предоставления полного исходного кода.
+
+1. Компонент UploadDagComponent
+Описание:
+
+Этот компонент отвечает за загрузку DAG-файлов пользователями и их сохранение в GitHub репозитории через GitHub API.
+
+Основные функции:
+
+Форма для загрузки файлов.
+Валидация загружаемых файлов.
+Обработка загрузки и отправка файла в GitHub репозиторий.
+Сохранение информации о DAG в базе данных.
+Реализация:
+
+Создание компонента:
+
+bash
+Копировать код
+php artisan make:livewire UploadDagComponent
+Свойства компонента:
+
+$file – для хранения загружаемого файла.
+$dagName – имя DAG (если требуется).
+Методы компонента:
+
+rules() – для определения правил валидации:
+
+php
+Копировать код
+protected $rules = [
+ 'file' => 'required|file|mimes:py|max:1024', // Максимум 1MB
+];
+uploadDag() – основной метод для обработки загрузки:
+
+Выполнить валидацию файла.
+Прочитать содержимое файла.
+Использовать GitHub API для загрузки файла в репозиторий (как описано в вашем основном коде).
+Сохранить информацию о DAG в базе данных, связав его с текущим пользователем (auth()->user()->id).
+Шаблон компонента (Blade):
+
+Форма с полем для выбора файла:
+
+html
+Копировать код
+
+Отображение сообщений об успехе или ошибках.
+
+2. Компонент DagListComponent
+Описание:
+
+Этот компонент отображает список DAG-файлов, загруженных текущим пользователем.
+
+Основные функции:
+
+Вывод списка DAG с информацией о каждом файле.
+Возможность удаления или обновления DAG (опционально).
+Пагинация, если количество DAG большое.
+Реализация:
+
+Создание компонента:
+
+bash
+Копировать код
+php artisan make:livewire DagListComponent
+Свойства компонента:
+
+$dags – коллекция DAG-файлов пользователя.
+Методы компонента:
+
+mount() или render() – для загрузки списка DAG:
+
+php
+Копировать код
+public function render()
+{
+ $this->dags = Dag::where('user_id', auth()->id())->get();
+ return view('livewire.dag-list-component');
+}
+deleteDag($dagId) – метод для удаления DAG (если необходимо).
+
+Шаблон компонента (Blade):
+
+Таблица или список для отображения DAG:
+
+html
+Копировать код
+
+
+
+
Имя файла
+
Дата загрузки
+
Действия
+
+
+
+ @foreach($dags as $dag)
+
+
{{ $dag->filename }}
+
{{ $dag->created_at }}
+
+
+
+
+ @endforeach
+
+
+3. Компонент DagStatusComponent
+Описание:
+
+Отображает текущий статус выполнения DAG, получая данные из Airflow API.
+
+Основные функции:
+
+Получение статусов DAG из Airflow API.
+Обновление статусов в реальном времени.
+Отображение статусов в удобном формате.
+Реализация:
+
+Создание компонента:
+
+bash
+Копировать код
+php artisan make:livewire DagStatusComponent
+Свойства компонента:
+
+$dagStatuses – массив статусов DAG.
+Методы компонента:
+
+mount() – для инициализации данных.
+
+getDagStatuses() – метод для обращения к Airflow API:
+
+php
+Копировать код
+public function getDagStatuses()
+{
+ $dags = Dag::where('user_id', auth()->id())->get();
+ foreach ($dags as $dag) {
+ $response = Http::withBasicAuth('airflow_username', 'airflow_password')
+ ->get("http://airflow-server:8080/api/v1/dags/{$dag->dag_id}/dagRuns");
+ if ($response->successful()) {
+ $this->dagStatuses[$dag->dag_id] = $response->json();
+ }
+ }
+}
+Вы можете использовать wire:poll для автоматического обновления статусов:
+
+html
+Копировать код
+
+4. Компонент DagQueueComponent
+Описание:
+
+Отображает очередь задач DAG для текущего пользователя, используя данные из Airflow API.
+
+Основные функции:
+
+Получение информации о запланированных и выполняемых задачах.
+Обновление данных в реальном времени.
+Реализация:
+
+Создание компонента:
+
+bash
+Копировать код
+php artisan make:livewire DagQueueComponent
+Свойства компонента:
+
+$dagQueue – массив задач в очереди.
+Методы компонента:
+
+getDagQueue() – обращение к Airflow API для получения очереди задач:
+
+php
+Копировать код
+public function getDagQueue()
+{
+ $response = Http::withBasicAuth('airflow_username', 'airflow_password')
+ ->get('http://airflow-server:8080/api/v1/queues');
+ if ($response->successful()) {
+ $this->dagQueue = $response->json();
+ }
+}
+Используйте wire:poll для обновления данных:
+
+html
+Копировать код
+
+ @foreach($dagQueue as $task)
+ @if(in_array($task['dag_id'], $userDagIds))
+
Задача {{ $task['task_id'] }} в DAG {{ $task['dag_id'] }}
+ @endif
+ @endforeach
+
+Здесь $userDagIds – массив dag_id текущего пользователя.
+
+Общие рекомендации для компонентов
+Валидация и безопасность:
+
+Всегда проверяйте права доступа, чтобы пользователи могли взаимодействовать только со своими данными.
+Используйте методы authorize() или проверки внутри методов компонентов.
+Обработка ошибок:
+
+Обрабатывайте возможные исключения при работе с внешними API.
+Предоставляйте пользователям понятные сообщения об ошибках.
+Пользовательский интерфейс:
+
+Делайте интерфейс интуитивно понятным и отзывчивым.
+Используйте стандартные компоненты UI или фреймворки CSS, такие как Tailwind CSS (который идет с Jetstream).
+Интеграция компонентов в приложение
+Маршруты и контроллеры:
+
+Определите маршруты в web.php для страниц, где будут использоваться компоненты.
+
+php
+Копировать код
+Route::middleware(['auth:sanctum', 'verified'])->get('/dashboard', function () {
+ return view('dashboard');
+})->name('dashboard');
+Шаблоны Blade:
+
+Вставьте компоненты в соответствующие шаблоны.
+
+html
+Копировать код
+
+@extends('layouts.app')
+
+@section('content')
+
+
+
+
+
+
+@endsection
+Дополнительные настройки
+Конфигурация Airflow API:
+
+Убедитесь, что Airflow API доступен и настроен для приема запросов от вашего приложения.
+
+Добавьте необходимые переменные в .env файл для хранения URL и данных аутентификации:
+
+makefile
+Копировать код
+AIRFLOW_API_URL=http://airflow-server:8080/api/v1
+AIRFLOW_API_USER=airflow_username
+AIRFLOW_API_PASSWORD=airflow_password
+Конфигурация GitHub API:
+
+Обновите конфигурацию GitHub API, если требуется, и храните токен в .env файле:
+
+makefile
+Копировать код
+GITHUB_TOKEN=ваш_токен
+GITHUB_USERNAME=ваш_логин
+GITHUB_REPOSITORY=имя_репозитория
+GITHUB_BRANCH=main
+Модели и миграции:
+
+Создайте модель Dag и соответствующую миграцию:
+
+bash
+Копировать код
+php artisan make:model Dag -m
+Поля таблицы dags могут включать:
+
+id
+user_id
+dag_id или filename
+created_at
+updated_at
+Не забудьте установить связь между моделями User и Dag.
+
+php
+Копировать код
+// В модели User
+public function dags()
+{
+ return $this->hasMany(Dag::class);
+}
+php
+Копировать код
+// В модели Dag
+public function user()
+{
+ return $this->belongsTo(User::class);
+}
+Заключение
+Следуя этим инструкциям, вы сможете создать полноценное приложение на Laravel Jetstream Livewire, которое позволит пользователям:
+
+Загружать DAG-файлы без прямого доступа к GitHub репозиторию.
+Просматривать список своих DAG-файлов.
+Отслеживать статусы выполнения и очередь задач своих DAG в Airflow.
+Не забудьте протестировать каждую часть приложения и убедиться, что все компоненты работают корректно вместе.
+
+Извиняюсь за недочет. Сейчас я опишу более конкретные шаги разработки приложения с финальным исходным кодом.
+
+### Полный пошаговый план разработки приложения с исходным кодом:
+
+---
+
+### **Шаг 1: Установка Laravel и Jetstream с Livewire**
+
+1. **Создание нового проекта Laravel:**
+ ```bash
+ composer create-project laravel/laravel airflow-dag-manager
+ ```
+
+2. **Установка Jetstream с Livewire:**
+ ```bash
+ composer require laravel/jetstream
+ php artisan jetstream:install livewire
+ npm install
+ npm run dev
+ php artisan migrate
+ ```
+
+3. **Настройка аутентификации:**
+ Jetstream автоматически добавит базовые функции аутентификации.
+
+---
+
+### **Шаг 2: Настройка ролей и прав доступа**
+
+1. Установка пакета Spatie Laravel-Permission для управления ролями и правами доступа:
+ ```bash
+ composer require spatie/laravel-permission
+ ```
+
+2. Публикация конфигурации и выполнение миграций:
+ ```bash
+ php artisan vendor:publish --provider="Spatie\Permission\PermissionServiceProvider"
+ php artisan migrate
+ ```
+
+3. **Настройка ролей и прав:**
+ Добавьте роли, например, `admin` и `user`, а также права, такие как `upload dag`, `view status`. Пример настройки ролей можно добавить в seeder:
+
+ ```php
+ use Spatie\Permission\Models\Role;
+ use Spatie\Permission\Models\Permission;
+
+ // В seeder:
+ Role::create(['name' => 'admin']);
+ Role::create(['name' => 'user']);
+
+ Permission::create(['name' => 'upload dag']);
+ Permission::create(['name' => 'view status']);
+ ```
+
+---
+
+### **Шаг 3: Интеграция с GitHub API**
+
+1. **Создание GitHub Personal Access Token:**
+ Перейдите в GitHub, создайте новый токен с правами `repo` для доступа к репозиторию. Сохраните его в `.env`:
+ ```env
+ GITHUB_TOKEN=ваш_токен
+ GITHUB_USERNAME=ваш_логин
+ GITHUB_REPOSITORY=имя_репозитория
+ GITHUB_BRANCH=main
+ ```
+
+2. **Установка библиотеки для работы с GitHub API:**
+ ```bash
+ composer require knplabs/github-api
+ ```
+
+3. **Создание метода для загрузки DAG в репозиторий:**
+
+ Создайте компонент **UploadDagComponent** для загрузки DAG:
+
+ ```bash
+ php artisan make:livewire UploadDagComponent
+ ```
+
+ **Код компонента UploadDagComponent:**
+ ```php
+ 'required|file|mimes:py|max:1024',
+ ];
+
+ public function uploadDag()
+ {
+ $this->validate();
+
+ // Сохранение временного файла
+ $path = $this->dagFile->store('temp_dags');
+
+ // Получение содержимого файла
+ $filename = $this->dagFile->getClientOriginalName();
+ $content = Storage::get($path);
+
+ // Интеграция с GitHub API
+ $client = new Client();
+ $client->authenticate(env('GITHUB_TOKEN'), null, Client::AUTH_ACCESS_TOKEN);
+
+ $username = env('GITHUB_USERNAME');
+ $repository = env('GITHUB_REPOSITORY');
+ $branch = env('GITHUB_BRANCH');
+ $dagPath = 'dags/' . $filename;
+
+ // Получение SHA последнего коммита
+ $reference = $client->api('gitData')->references()->show($username, $repository, 'heads/' . $branch);
+ $commitSha = $reference['object']['sha'];
+
+ // Создание нового блоба
+ $blob = $client->api('gitData')->blobs()->create($username, $repository, [
+ 'content' => base64_encode($content),
+ 'encoding' => 'base64',
+ ]);
+
+ // Создание нового дерева
+ $tree = $client->api('gitData')->trees()->create($username, $repository, [
+ 'base_tree' => $commitSha,
+ 'tree' => [
+ [
+ 'path' => $dagPath,
+ 'mode' => '100644',
+ 'type' => 'blob',
+ 'sha' => $blob['sha'],
+ ],
+ ],
+ ]);
+
+ // Создание нового коммита
+ $commit = $client->api('gitData')->commits()->create($username, $repository, [
+ 'message' => 'Добавлен DAG ' . $filename,
+ 'tree' => $tree['sha'],
+ 'parents' => [$commitSha],
+ ]);
+
+ // Обновление ссылки
+ $client->api('gitData')->references()->update($username, $repository, 'heads/' . $branch, [
+ 'sha' => $commit['sha'],
+ ]);
+
+ // Удаление временного файла
+ Storage::delete($path);
+
+ // Сообщение об успехе
+ session()->flash('message', 'DAG успешно загружен и отправлен в репозиторий GitHub.');
+ }
+
+ public function render()
+ {
+ return view('livewire.upload-dag-component');
+ }
+ }
+ ```
+
+4. **Шаблон компонента (Blade):**
+ ```blade
+
+ @if (session()->has('message'))
+
+ {{ session('message') }}
+
+ @endif
+
+
+
+ ```
+
+---
+
+### **Шаг 4: Синхронизация DAG с Airflow**
+
+1. **Настройка Airflow для получения DAG:**
+ Вы можете настроить Airflow для автоматического выполнения `git pull` либо использовать GitHub Actions для деплоя DAG на сервер Airflow.
+
+---
+
+### **Шаг 5: Получение статусов выполнения DAG через Airflow API**
+
+1. **Включение Airflow API:**
+ В `airflow.cfg` добавьте или активируйте следующие строки:
+ ```ini
+ [api]
+ auth_backend = airflow.api.auth.backend.basic_auth
+ ```
+
+2. **Запросы к Airflow API для получения статусов DAG:**
+
+ Создайте компонент **DagStatusComponent**:
+
+ ```bash
+ php artisan make:livewire DagStatusComponent
+ ```
+
+ **Код компонента DagStatusComponent:**
+ ```php
+ fetchDagStatuses();
+ }
+
+ public function fetchDagStatuses()
+ {
+ $response = Http::withBasicAuth(env('AIRFLOW_USERNAME'), env('AIRFLOW_PASSWORD'))
+ ->get(env('AIRFLOW_API_URL') . '/api/v1/dags/{dag_id}/dagRuns');
+
+ if ($response->ok()) {
+ $this->dagRuns = $response->json();
+ }
+ }
+
+ public function render()
+ {
+ return view('livewire.dag-status-component');
+ }
+ }
+ ```
+
+3. **Шаблон компонента (Blade):**
+ ```blade
+
+
Статусы выполнения DAG
+
+
+
+
+
DAG Run ID
+
Старт
+
Завершение
+
Статус
+
+
+
+ @foreach($dagRuns as $run)
+
+
{{ $run['dag_run_id'] }}
+
{{ $run['start_date'] }}
+
{{ $run['end_date'] }}
+
{{ $run['state'] }}
+
+ @endforeach
+
+
+
+ ```
+
+---
+
+### **Шаг 6: Безопасность и тестирование**
+
+1. **Используйте `.env` для хранения токенов и конфиденциальных данных.**
+2. **Проверьте приложение с помощью тестов и настройте деплой на сервер.**
+
+---
+
+В процессе переписывания не упускай важные детали и сохраняй исходный код, а также комманды терминала чтобы план был понятен и подробен