feat(graph): add interactive relation details and synchronized labels
This commit is contained in:
parent
a683266468
commit
8d5a5910db
12 changed files with 642 additions and 10 deletions
|
|
@ -93,6 +93,17 @@ gboolean relation_dao_insert(
|
||||||
GError **error
|
GError **error
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Met à jour les champs modifiables d'une relation existante.
|
||||||
|
*
|
||||||
|
* L'identifiant, les extrémités et la date de création sont conservés.
|
||||||
|
*/
|
||||||
|
gboolean relation_dao_update(
|
||||||
|
RelationDao *relation_dao,
|
||||||
|
const RelationRecord *relation_record,
|
||||||
|
GError **error
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Recherche une relation par son UUID.
|
* @brief Recherche une relation par son UUID.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -14,6 +14,7 @@ G_BEGIN_DECLS
|
||||||
* @brief Résultat opaque produit par le dialogue.
|
* @brief Résultat opaque produit par le dialogue.
|
||||||
*/
|
*/
|
||||||
typedef struct CreateRelationDialogResult CreateRelationDialogResult;
|
typedef struct CreateRelationDialogResult CreateRelationDialogResult;
|
||||||
|
typedef struct RelationRecord RelationRecord;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Codes d'erreur produits par CreateRelationDialog.
|
* @brief Codes d'erreur produits par CreateRelationDialog.
|
||||||
|
|
@ -82,6 +83,21 @@ gboolean create_relation_dialog_present(
|
||||||
GError **error
|
GError **error
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @brief Présente le dialogue prérempli pour modifier une relation.
|
||||||
|
*
|
||||||
|
* La source et la cible sont conservées ; les champs métier sont modifiables.
|
||||||
|
* Le dialogue ne réalise aucune écriture SQLite.
|
||||||
|
*/
|
||||||
|
gboolean edit_relation_dialog_present(
|
||||||
|
GtkWindow *parent,
|
||||||
|
const RelationRecord *relation_record,
|
||||||
|
const GPtrArray *entities,
|
||||||
|
CreateRelationDialogCallback callback,
|
||||||
|
gpointer user_data,
|
||||||
|
GError **error
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Valide les valeurs obligatoires d'une relation.
|
* @brief Valide les valeurs obligatoires d'une relation.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -137,6 +137,12 @@ typedef void (*MainWindowAddRelationCallback)(
|
||||||
gpointer user_data
|
gpointer user_data
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** @brief Callback appelé pour modifier une relation. */
|
||||||
|
typedef void (*MainWindowEditRelationCallback)(
|
||||||
|
const char *relation_identifier,
|
||||||
|
gpointer user_data
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Callback appelé lors du déclenchement d'une action OSINT.
|
* @brief Callback appelé lors du déclenchement d'une action OSINT.
|
||||||
*
|
*
|
||||||
|
|
@ -227,6 +233,13 @@ void main_window_set_add_relation_callback(
|
||||||
gpointer user_data
|
gpointer user_data
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** @brief Définit le callback de modification d'une relation. */
|
||||||
|
void main_window_set_edit_relation_callback(
|
||||||
|
MainWindow *main_window,
|
||||||
|
MainWindowEditRelationCallback callback,
|
||||||
|
gpointer user_data
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Définit le callback de déclenchement des actions OSINT.
|
* @brief Définit le callback de déclenchement des actions OSINT.
|
||||||
*
|
*
|
||||||
|
|
@ -419,6 +432,12 @@ void main_window_set_graph(
|
||||||
const InvestigationGraphLayout *graph_layout
|
const InvestigationGraphLayout *graph_layout
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** @brief Sélectionne une relation dans le graphe affiché. */
|
||||||
|
gboolean main_window_select_graph_relation(
|
||||||
|
MainWindow *main_window,
|
||||||
|
const char *relation_identifier
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Affiche une erreur de chargement du graphe.
|
* @brief Affiche une erreur de chargement du graphe.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -78,6 +78,12 @@ typedef void (*WorkspaceAddRelationCallback)(
|
||||||
gpointer user_data
|
gpointer user_data
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** @brief Callback appelé pour modifier la relation sélectionnée. */
|
||||||
|
typedef void (*WorkspaceEditRelationCallback)(
|
||||||
|
const char *relation_identifier,
|
||||||
|
gpointer user_data
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Callback appelé lors du déclenchement d'une action OSINT.
|
* @brief Callback appelé lors du déclenchement d'une action OSINT.
|
||||||
*
|
*
|
||||||
|
|
@ -288,6 +294,13 @@ void workspace_set_add_relation_callback(
|
||||||
gpointer user_data
|
gpointer user_data
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/** @brief Définit le callback de modification d'une relation. */
|
||||||
|
void workspace_set_edit_relation_callback(
|
||||||
|
Workspace *workspace,
|
||||||
|
WorkspaceEditRelationCallback callback,
|
||||||
|
gpointer user_data
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Affiche l'état de chargement du graphe.
|
* @brief Affiche l'état de chargement du graphe.
|
||||||
*
|
*
|
||||||
|
|
|
||||||
|
|
@ -40,6 +40,7 @@
|
||||||
#include "views/osint_execution_history_dialog.h"
|
#include "views/osint_execution_history_dialog.h"
|
||||||
#include "dao/evidence_dao.h"
|
#include "dao/evidence_dao.h"
|
||||||
#include "dao/entity_dao.h"
|
#include "dao/entity_dao.h"
|
||||||
|
#include "dao/relation_dao.h"
|
||||||
#include "dao/osint_execution_dao.h"
|
#include "dao/osint_execution_dao.h"
|
||||||
#include "models/evidence_type.h"
|
#include "models/evidence_type.h"
|
||||||
#include "widgets/evidence_list_model.h"
|
#include "widgets/evidence_list_model.h"
|
||||||
|
|
@ -52,6 +53,7 @@
|
||||||
#include "core/eml_processing.h"
|
#include "core/eml_processing.h"
|
||||||
#include "core/eml_integration.h"
|
#include "core/eml_integration.h"
|
||||||
#include "database/database.h"
|
#include "database/database.h"
|
||||||
|
#include "database/transaction.h"
|
||||||
#include "views/create_social_account_dialog.h"
|
#include "views/create_social_account_dialog.h"
|
||||||
#include "views/create_person_dialog.h"
|
#include "views/create_person_dialog.h"
|
||||||
#include "views/eml_analysis_dialog.h"
|
#include "views/eml_analysis_dialog.h"
|
||||||
|
|
@ -123,6 +125,7 @@ struct Application
|
||||||
guint64 graph_load_generation;
|
guint64 graph_load_generation;
|
||||||
|
|
||||||
char *selected_evidence_identifier;
|
char *selected_evidence_identifier;
|
||||||
|
char *pending_relation_selection_identifier;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -737,6 +740,14 @@ static void application_on_graph_loaded(
|
||||||
application->graph_layout
|
application->graph_layout
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (application->pending_relation_selection_identifier != NULL)
|
||||||
|
{
|
||||||
|
main_window_select_graph_relation(application->main_window,
|
||||||
|
application->pending_relation_selection_identifier);
|
||||||
|
g_clear_pointer(&application->pending_relation_selection_identifier,
|
||||||
|
g_free);
|
||||||
|
}
|
||||||
|
|
||||||
status_message =
|
status_message =
|
||||||
g_strdup_printf(
|
g_strdup_printf(
|
||||||
"Graphe chargé : %" G_GUINT64_FORMAT
|
"Graphe chargé : %" G_GUINT64_FORMAT
|
||||||
|
|
@ -4917,6 +4928,136 @@ cleanup:
|
||||||
/**
|
/**
|
||||||
* @brief Ouvre le dialogue de création depuis la fiche d'une entité.
|
* @brief Ouvre le dialogue de création depuis la fiche d'une entité.
|
||||||
*/
|
*/
|
||||||
|
/** @brief Contexte conservé pendant l'édition d'une relation. */
|
||||||
|
typedef struct
|
||||||
|
{
|
||||||
|
Application *application;
|
||||||
|
char *relation_identifier;
|
||||||
|
} ApplicationEditRelationContext;
|
||||||
|
|
||||||
|
/** @brief Libère le contexte d'édition d'une relation. */
|
||||||
|
static void application_edit_relation_context_free(
|
||||||
|
ApplicationEditRelationContext *context)
|
||||||
|
{
|
||||||
|
if (context == NULL) return;
|
||||||
|
g_free(context->relation_identifier);
|
||||||
|
g_free(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief Persiste les valeurs validées du dialogue d'édition. */
|
||||||
|
static void application_on_edit_relation_completed(
|
||||||
|
CreateRelationDialogResult *result, gpointer user_data)
|
||||||
|
{
|
||||||
|
ApplicationEditRelationContext *context = user_data;
|
||||||
|
Application *application = context != NULL ? context->application : NULL;
|
||||||
|
Database *database = NULL;
|
||||||
|
RelationDao *relation_dao = NULL;
|
||||||
|
RelationRecord *existing = NULL;
|
||||||
|
RelationRecord *updated = NULL;
|
||||||
|
const InvestigationProject *project = NULL;
|
||||||
|
GDateTime *now = NULL;
|
||||||
|
char *timestamp = NULL;
|
||||||
|
GError *error = NULL;
|
||||||
|
gboolean transaction_active = FALSE;
|
||||||
|
|
||||||
|
if (result == NULL || application == NULL || application->session == NULL)
|
||||||
|
goto cleanup;
|
||||||
|
database = investigation_session_get_database(application->session);
|
||||||
|
project = investigation_session_get_project(application->session);
|
||||||
|
relation_dao = relation_dao_new(database, &error);
|
||||||
|
if (relation_dao == NULL) goto failure;
|
||||||
|
existing = relation_dao_find_by_identifier(relation_dao,
|
||||||
|
context->relation_identifier, &error);
|
||||||
|
if (existing == NULL) goto failure;
|
||||||
|
now = g_date_time_new_now_utc();
|
||||||
|
timestamp = now != NULL ? g_date_time_format(now,
|
||||||
|
"%Y-%m-%dT%H:%M:%SZ") : NULL;
|
||||||
|
updated = relation_record_new(
|
||||||
|
relation_record_get_identifier(existing),
|
||||||
|
relation_record_get_source_entity_identifier(existing),
|
||||||
|
relation_record_get_target_entity_identifier(existing),
|
||||||
|
create_relation_dialog_result_get_relation_type(result),
|
||||||
|
create_relation_dialog_result_get_label(result),
|
||||||
|
create_relation_dialog_result_get_justification(result),
|
||||||
|
create_relation_dialog_result_get_confidence(result),
|
||||||
|
relation_record_get_created_at(existing), timestamp,
|
||||||
|
relation_record_get_status(existing), &error);
|
||||||
|
if (updated == NULL || !database_transaction_begin(database)) goto failure;
|
||||||
|
transaction_active = TRUE;
|
||||||
|
if (!relation_dao_update(relation_dao, updated, &error) ||
|
||||||
|
!database_transaction_commit(database))
|
||||||
|
goto failure;
|
||||||
|
transaction_active = FALSE;
|
||||||
|
main_window_set_status(application->main_window,
|
||||||
|
"Relation modifiée. Actualisation du graphe…");
|
||||||
|
g_free(application->pending_relation_selection_identifier);
|
||||||
|
application->pending_relation_selection_identifier = g_strdup(
|
||||||
|
context->relation_identifier);
|
||||||
|
application_start_graph_loading(application,
|
||||||
|
investigation_project_get_database_path(project));
|
||||||
|
goto cleanup;
|
||||||
|
|
||||||
|
failure:
|
||||||
|
if (transaction_active) database_transaction_rollback(database);
|
||||||
|
application_present_error(application, "Modification impossible",
|
||||||
|
error != NULL ? error->message :
|
||||||
|
"La relation n'a pas pu être modifiée.");
|
||||||
|
cleanup:
|
||||||
|
g_clear_error(&error);
|
||||||
|
g_free(timestamp);
|
||||||
|
if (now != NULL) g_date_time_unref(now);
|
||||||
|
relation_record_free(updated);
|
||||||
|
relation_record_free(existing);
|
||||||
|
relation_dao_free(relation_dao);
|
||||||
|
create_relation_dialog_result_free(result);
|
||||||
|
application_edit_relation_context_free(context);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief Ouvre le formulaire prérempli de la relation sélectionnée. */
|
||||||
|
static void application_on_edit_relation_requested(
|
||||||
|
const char *relation_identifier, gpointer user_data)
|
||||||
|
{
|
||||||
|
Application *application = user_data;
|
||||||
|
Database *database = NULL;
|
||||||
|
RelationDao *relation_dao = NULL;
|
||||||
|
EntityDao *entity_dao = NULL;
|
||||||
|
RelationRecord *relation = NULL;
|
||||||
|
GPtrArray *entities = NULL;
|
||||||
|
ApplicationEditRelationContext *context = NULL;
|
||||||
|
GError *error = NULL;
|
||||||
|
|
||||||
|
if (application == NULL || application->session == NULL ||
|
||||||
|
relation_identifier == NULL) return;
|
||||||
|
database = investigation_session_get_database(application->session);
|
||||||
|
relation_dao = relation_dao_new(database, &error);
|
||||||
|
entity_dao = entity_dao_new(database, &error);
|
||||||
|
if (relation_dao != NULL)
|
||||||
|
relation = relation_dao_find_by_identifier(relation_dao,
|
||||||
|
relation_identifier, &error);
|
||||||
|
if (entity_dao != NULL) entities = entity_dao_list_all(entity_dao, &error);
|
||||||
|
if (relation == NULL || entities == NULL) goto failure;
|
||||||
|
context = g_new0(ApplicationEditRelationContext, 1);
|
||||||
|
context->application = application;
|
||||||
|
context->relation_identifier = g_strdup(relation_identifier);
|
||||||
|
if (context->relation_identifier == NULL || !edit_relation_dialog_present(
|
||||||
|
main_window_get_window(application->main_window), relation, entities,
|
||||||
|
application_on_edit_relation_completed, context, &error))
|
||||||
|
goto failure;
|
||||||
|
context = NULL;
|
||||||
|
goto cleanup;
|
||||||
|
failure:
|
||||||
|
application_present_error(application, "Modification impossible",
|
||||||
|
error != NULL ? error->message :
|
||||||
|
"Le formulaire de modification n'a pas pu être ouvert.");
|
||||||
|
cleanup:
|
||||||
|
application_edit_relation_context_free(context);
|
||||||
|
g_clear_error(&error);
|
||||||
|
g_clear_pointer(&entities, g_ptr_array_unref);
|
||||||
|
relation_record_free(relation);
|
||||||
|
entity_dao_free(entity_dao);
|
||||||
|
relation_dao_free(relation_dao);
|
||||||
|
}
|
||||||
|
|
||||||
static void application_on_add_relation_requested(
|
static void application_on_add_relation_requested(
|
||||||
const char *source_entity_identifier,
|
const char *source_entity_identifier,
|
||||||
gpointer user_data
|
gpointer user_data
|
||||||
|
|
@ -6333,6 +6474,9 @@ static void application_on_activate(
|
||||||
application
|
application
|
||||||
);
|
);
|
||||||
|
|
||||||
|
main_window_set_edit_relation_callback(application->main_window,
|
||||||
|
application_on_edit_relation_requested, application);
|
||||||
|
|
||||||
main_window_set_osint_action_callback(
|
main_window_set_osint_action_callback(
|
||||||
application->main_window,
|
application->main_window,
|
||||||
application_on_osint_action_requested,
|
application_on_osint_action_requested,
|
||||||
|
|
@ -6606,6 +6750,8 @@ void application_free(
|
||||||
&application->selected_evidence_identifier,
|
&application->selected_evidence_identifier,
|
||||||
g_free
|
g_free
|
||||||
);
|
);
|
||||||
|
g_clear_pointer(&application->pending_relation_selection_identifier,
|
||||||
|
g_free);
|
||||||
|
|
||||||
g_free(
|
g_free(
|
||||||
application
|
application
|
||||||
|
|
|
||||||
|
|
@ -52,6 +52,12 @@ static const char *const relation_dao_insert_sql =
|
||||||
" ?"
|
" ?"
|
||||||
");";
|
");";
|
||||||
|
|
||||||
|
/** @brief Requête de modification des attributs d'une relation. */
|
||||||
|
static const char *const relation_dao_update_sql =
|
||||||
|
"UPDATE relations SET "
|
||||||
|
"type_relation = ?, label = ?, justification = ?, confiance = ?, "
|
||||||
|
"updated_at = ?, status = ? WHERE id = ?;";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Requête de recherche par UUID.
|
* @brief Requête de recherche par UUID.
|
||||||
*/
|
*/
|
||||||
|
|
@ -1298,6 +1304,65 @@ cleanup:
|
||||||
return success;
|
return success;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gboolean relation_dao_update(RelationDao *relation_dao,
|
||||||
|
const RelationRecord *relation_record, GError **error)
|
||||||
|
{
|
||||||
|
DatabaseStatement *statement = NULL;
|
||||||
|
const char *status_text = NULL;
|
||||||
|
gboolean success = FALSE;
|
||||||
|
|
||||||
|
g_return_val_if_fail(error == NULL || *error == NULL, FALSE);
|
||||||
|
if (relation_dao == NULL || relation_record == NULL)
|
||||||
|
{
|
||||||
|
relation_dao_set_error_literal(error,
|
||||||
|
RELATION_DAO_ERROR_INVALID_ARGUMENT,
|
||||||
|
"La relation à modifier est invalide.");
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
status_text = relation_dao_status_to_text(
|
||||||
|
relation_record_get_status(relation_record));
|
||||||
|
statement = database_statement_prepare(relation_dao->database,
|
||||||
|
relation_dao_update_sql);
|
||||||
|
if (statement == NULL)
|
||||||
|
{
|
||||||
|
relation_dao_set_database_error(relation_dao, error,
|
||||||
|
RELATION_DAO_ERROR_PREPARE,
|
||||||
|
"Impossible de préparer la modification de la relation");
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
if (!database_statement_bind_text(statement, 1,
|
||||||
|
relation_record_get_relation_type(relation_record)) ||
|
||||||
|
!relation_dao_bind_optional_text(statement, 2,
|
||||||
|
relation_record_get_label(relation_record)) ||
|
||||||
|
!relation_dao_bind_optional_text(statement, 3,
|
||||||
|
relation_record_get_justification(relation_record)) ||
|
||||||
|
!database_statement_bind_int64(statement, 4,
|
||||||
|
relation_record_get_confidence(relation_record)) ||
|
||||||
|
!database_statement_bind_text(statement, 5,
|
||||||
|
relation_record_get_updated_at(relation_record)) ||
|
||||||
|
status_text == NULL ||
|
||||||
|
!database_statement_bind_text(statement, 6, status_text) ||
|
||||||
|
!database_statement_bind_text(statement, 7,
|
||||||
|
relation_record_get_identifier(relation_record)))
|
||||||
|
{
|
||||||
|
relation_dao_set_database_error(relation_dao, error,
|
||||||
|
RELATION_DAO_ERROR_BIND,
|
||||||
|
"Impossible de lier la modification de la relation");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
if (database_statement_step(statement) != DATABASE_STATEMENT_STEP_DONE)
|
||||||
|
{
|
||||||
|
relation_dao_set_database_error(relation_dao, error,
|
||||||
|
RELATION_DAO_ERROR_EXECUTE,
|
||||||
|
"Impossible de modifier la relation");
|
||||||
|
goto cleanup;
|
||||||
|
}
|
||||||
|
success = TRUE;
|
||||||
|
cleanup:
|
||||||
|
database_statement_finalize(statement);
|
||||||
|
return success;
|
||||||
|
}
|
||||||
|
|
||||||
RelationRecord *relation_dao_find_by_identifier(
|
RelationRecord *relation_dao_find_by_identifier(
|
||||||
RelationDao *relation_dao,
|
RelationDao *relation_dao,
|
||||||
const char *identifier,
|
const char *identifier,
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@
|
||||||
#include "views/create_relation_dialog.h"
|
#include "views/create_relation_dialog.h"
|
||||||
|
|
||||||
#include "models/entity_record.h"
|
#include "models/entity_record.h"
|
||||||
|
#include "models/relation_record.h"
|
||||||
|
|
||||||
#include <glib.h>
|
#include <glib.h>
|
||||||
|
|
||||||
|
|
@ -683,9 +684,10 @@ cleanup:
|
||||||
return valid;
|
return valid;
|
||||||
}
|
}
|
||||||
|
|
||||||
gboolean create_relation_dialog_present(
|
static gboolean create_relation_dialog_present_internal(
|
||||||
GtkWindow *parent,
|
GtkWindow *parent,
|
||||||
const char *source_entity_identifier,
|
const char *source_entity_identifier,
|
||||||
|
const RelationRecord *existing_relation,
|
||||||
const GPtrArray *entities,
|
const GPtrArray *entities,
|
||||||
CreateRelationDialogCallback callback,
|
CreateRelationDialogCallback callback,
|
||||||
gpointer user_data,
|
gpointer user_data,
|
||||||
|
|
@ -746,6 +748,9 @@ gboolean create_relation_dialog_present(
|
||||||
guint entity_index =
|
guint entity_index =
|
||||||
0;
|
0;
|
||||||
|
|
||||||
|
guint selected_target_index =
|
||||||
|
0;
|
||||||
|
|
||||||
g_return_val_if_fail(
|
g_return_val_if_fail(
|
||||||
error == NULL || *error == NULL,
|
error == NULL || *error == NULL,
|
||||||
FALSE
|
FALSE
|
||||||
|
|
@ -900,6 +905,13 @@ gboolean create_relation_dialog_present(
|
||||||
entity_identifier
|
entity_identifier
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (existing_relation != NULL && g_strcmp0(entity_identifier,
|
||||||
|
relation_record_get_target_entity_identifier(
|
||||||
|
existing_relation)) == 0)
|
||||||
|
{
|
||||||
|
selected_target_index = state->target_identifiers->len;
|
||||||
|
}
|
||||||
|
|
||||||
if (display_text == NULL ||
|
if (display_text == NULL ||
|
||||||
identifier_copy == NULL)
|
identifier_copy == NULL)
|
||||||
{
|
{
|
||||||
|
|
@ -971,7 +983,9 @@ gboolean create_relation_dialog_present(
|
||||||
|
|
||||||
gtk_window_set_title(
|
gtk_window_set_title(
|
||||||
state->window,
|
state->window,
|
||||||
"Ajouter une relation"
|
existing_relation != NULL
|
||||||
|
? "Modifier la relation"
|
||||||
|
: "Ajouter une relation"
|
||||||
);
|
);
|
||||||
|
|
||||||
gtk_window_set_transient_for(
|
gtk_window_set_transient_for(
|
||||||
|
|
@ -1030,7 +1044,9 @@ gboolean create_relation_dialog_present(
|
||||||
GTK_LABEL(
|
GTK_LABEL(
|
||||||
title_label
|
title_label
|
||||||
),
|
),
|
||||||
"<b>Créer une relation entre deux entités</b>"
|
existing_relation != NULL
|
||||||
|
? "<b>Modifier la relation</b>"
|
||||||
|
: "<b>Créer une relation entre deux entités</b>"
|
||||||
);
|
);
|
||||||
|
|
||||||
gtk_label_set_xalign(
|
gtk_label_set_xalign(
|
||||||
|
|
@ -1179,9 +1195,12 @@ gboolean create_relation_dialog_present(
|
||||||
|
|
||||||
gtk_drop_down_set_selected(
|
gtk_drop_down_set_selected(
|
||||||
state->target_drop_down,
|
state->target_drop_down,
|
||||||
0
|
selected_target_index
|
||||||
);
|
);
|
||||||
|
|
||||||
|
gtk_widget_set_sensitive(GTK_WIDGET(state->target_drop_down),
|
||||||
|
existing_relation == NULL);
|
||||||
|
|
||||||
gtk_widget_set_hexpand(
|
gtk_widget_set_hexpand(
|
||||||
GTK_WIDGET(
|
GTK_WIDGET(
|
||||||
state->target_drop_down
|
state->target_drop_down
|
||||||
|
|
@ -1220,7 +1239,9 @@ gboolean create_relation_dialog_present(
|
||||||
|
|
||||||
gtk_spin_button_set_value(
|
gtk_spin_button_set_value(
|
||||||
state->confidence_spin_button,
|
state->confidence_spin_button,
|
||||||
80.0
|
existing_relation != NULL
|
||||||
|
? relation_record_get_confidence(existing_relation)
|
||||||
|
: 80.0
|
||||||
);
|
);
|
||||||
|
|
||||||
state->justification_text_view =
|
state->justification_text_view =
|
||||||
|
|
@ -1228,6 +1249,22 @@ gboolean create_relation_dialog_present(
|
||||||
gtk_text_view_new()
|
gtk_text_view_new()
|
||||||
);
|
);
|
||||||
|
|
||||||
|
if (existing_relation != NULL)
|
||||||
|
{
|
||||||
|
const char *relation_type = relation_record_get_relation_type(
|
||||||
|
existing_relation);
|
||||||
|
const char *label = relation_record_get_label(existing_relation);
|
||||||
|
const char *justification = relation_record_get_justification(
|
||||||
|
existing_relation);
|
||||||
|
gtk_editable_set_text(GTK_EDITABLE(state->relation_type_entry),
|
||||||
|
relation_type != NULL ? relation_type : "");
|
||||||
|
gtk_editable_set_text(GTK_EDITABLE(state->label_entry),
|
||||||
|
label != NULL ? label : "");
|
||||||
|
gtk_text_buffer_set_text(gtk_text_view_get_buffer(
|
||||||
|
state->justification_text_view),
|
||||||
|
justification != NULL ? justification : "", -1);
|
||||||
|
}
|
||||||
|
|
||||||
gtk_text_view_set_wrap_mode(
|
gtk_text_view_set_wrap_mode(
|
||||||
state->justification_text_view,
|
state->justification_text_view,
|
||||||
GTK_WRAP_WORD_CHAR
|
GTK_WRAP_WORD_CHAR
|
||||||
|
|
@ -1418,7 +1455,7 @@ gboolean create_relation_dialog_present(
|
||||||
|
|
||||||
create_button =
|
create_button =
|
||||||
gtk_button_new_with_label(
|
gtk_button_new_with_label(
|
||||||
"Ajouter"
|
existing_relation != NULL ? "Enregistrer" : "Ajouter"
|
||||||
);
|
);
|
||||||
|
|
||||||
gtk_widget_add_css_class(
|
gtk_widget_add_css_class(
|
||||||
|
|
@ -1543,6 +1580,30 @@ gboolean create_relation_dialog_present(
|
||||||
return TRUE;
|
return TRUE;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gboolean create_relation_dialog_present(GtkWindow *parent,
|
||||||
|
const char *source_entity_identifier, const GPtrArray *entities,
|
||||||
|
CreateRelationDialogCallback callback, gpointer user_data, GError **error)
|
||||||
|
{
|
||||||
|
return create_relation_dialog_present_internal(parent,
|
||||||
|
source_entity_identifier, NULL, entities, callback, user_data, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
gboolean edit_relation_dialog_present(GtkWindow *parent,
|
||||||
|
const RelationRecord *relation_record, const GPtrArray *entities,
|
||||||
|
CreateRelationDialogCallback callback, gpointer user_data, GError **error)
|
||||||
|
{
|
||||||
|
if (relation_record == NULL)
|
||||||
|
{
|
||||||
|
g_set_error_literal(error, CREATE_RELATION_DIALOG_ERROR,
|
||||||
|
CREATE_RELATION_DIALOG_ERROR_INVALID_ARGUMENT,
|
||||||
|
"La relation à modifier est invalide.");
|
||||||
|
return FALSE;
|
||||||
|
}
|
||||||
|
return create_relation_dialog_present_internal(parent,
|
||||||
|
relation_record_get_source_entity_identifier(relation_record),
|
||||||
|
relation_record, entities, callback, user_data, error);
|
||||||
|
}
|
||||||
|
|
||||||
void create_relation_dialog_result_free(
|
void create_relation_dialog_result_free(
|
||||||
CreateRelationDialogResult *result
|
CreateRelationDialogResult *result
|
||||||
)
|
)
|
||||||
|
|
|
||||||
|
|
@ -129,6 +129,9 @@ struct MainWindow
|
||||||
gpointer
|
gpointer
|
||||||
add_relation_user_data;
|
add_relation_user_data;
|
||||||
|
|
||||||
|
MainWindowEditRelationCallback edit_relation_callback;
|
||||||
|
gpointer edit_relation_user_data;
|
||||||
|
|
||||||
MainWindowOsintActionCallback
|
MainWindowOsintActionCallback
|
||||||
osint_action_callback;
|
osint_action_callback;
|
||||||
|
|
||||||
|
|
@ -509,6 +512,20 @@ static void main_window_on_add_relation_requested(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @brief Relaie la demande de modification d'une relation. */
|
||||||
|
static void main_window_on_edit_relation_requested(
|
||||||
|
const char *relation_identifier, gpointer user_data)
|
||||||
|
{
|
||||||
|
MainWindow *main_window = user_data;
|
||||||
|
if (main_window == NULL || main_window->edit_relation_callback == NULL ||
|
||||||
|
relation_identifier == NULL)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
main_window->edit_relation_callback(relation_identifier,
|
||||||
|
main_window->edit_relation_user_data);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Relaie la demande de vérification provenant du Workspace.
|
* @brief Relaie la demande de vérification provenant du Workspace.
|
||||||
*/
|
*/
|
||||||
|
|
@ -847,6 +864,9 @@ MainWindow *main_window_new(
|
||||||
main_window
|
main_window
|
||||||
);
|
);
|
||||||
|
|
||||||
|
workspace_set_edit_relation_callback(main_window->workspace,
|
||||||
|
main_window_on_edit_relation_requested, main_window);
|
||||||
|
|
||||||
workspace_set_osint_action_callback(
|
workspace_set_osint_action_callback(
|
||||||
main_window->workspace,
|
main_window->workspace,
|
||||||
main_window_on_osint_action_requested,
|
main_window_on_osint_action_requested,
|
||||||
|
|
@ -1252,6 +1272,14 @@ void main_window_set_graph(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
gboolean main_window_select_graph_relation(MainWindow *main_window,
|
||||||
|
const char *relation_identifier)
|
||||||
|
{
|
||||||
|
if (main_window == NULL) return FALSE;
|
||||||
|
return workspace_select_graph_relation(main_window->workspace,
|
||||||
|
relation_identifier);
|
||||||
|
}
|
||||||
|
|
||||||
void main_window_set_graph_error(
|
void main_window_set_graph_error(
|
||||||
MainWindow *main_window,
|
MainWindow *main_window,
|
||||||
const char *message
|
const char *message
|
||||||
|
|
@ -1614,6 +1642,14 @@ void main_window_set_add_relation_callback(
|
||||||
user_data;
|
user_data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void main_window_set_edit_relation_callback(MainWindow *main_window,
|
||||||
|
MainWindowEditRelationCallback callback, gpointer user_data)
|
||||||
|
{
|
||||||
|
if (main_window == NULL) return;
|
||||||
|
main_window->edit_relation_callback = callback;
|
||||||
|
main_window->edit_relation_user_data = user_data;
|
||||||
|
}
|
||||||
|
|
||||||
void main_window_set_quit_callback(
|
void main_window_set_quit_callback(
|
||||||
MainWindow *main_window,
|
MainWindow *main_window,
|
||||||
MainWindowQuitCallback callback,
|
MainWindowQuitCallback callback,
|
||||||
|
|
@ -1765,6 +1801,9 @@ void main_window_free(
|
||||||
NULL
|
NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
workspace_set_edit_relation_callback(main_window->workspace,
|
||||||
|
NULL, NULL);
|
||||||
|
|
||||||
main_window->graph_node_moved_callback =
|
main_window->graph_node_moved_callback =
|
||||||
NULL;
|
NULL;
|
||||||
|
|
||||||
|
|
@ -1783,6 +1822,9 @@ void main_window_free(
|
||||||
main_window->add_relation_user_data =
|
main_window->add_relation_user_data =
|
||||||
NULL;
|
NULL;
|
||||||
|
|
||||||
|
main_window->edit_relation_callback = NULL;
|
||||||
|
main_window->edit_relation_user_data = NULL;
|
||||||
|
|
||||||
main_window->show_graph_callback =
|
main_window->show_graph_callback =
|
||||||
NULL;
|
NULL;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -923,6 +923,7 @@ void entity_details_panel_set_entity(
|
||||||
),
|
),
|
||||||
TRUE
|
TRUE
|
||||||
);
|
);
|
||||||
|
gtk_widget_set_can_target(details_panel->root_revealer, TRUE);
|
||||||
|
|
||||||
g_free(
|
g_free(
|
||||||
confidence_text
|
confidence_text
|
||||||
|
|
@ -1009,6 +1010,8 @@ void entity_details_panel_clear(
|
||||||
),
|
),
|
||||||
FALSE
|
FALSE
|
||||||
);
|
);
|
||||||
|
/* Le revealer fermé ne doit pas bloquer le canvas sous l'overlay. */
|
||||||
|
gtk_widget_set_can_target(details_panel->root_revealer, FALSE);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -61,6 +61,9 @@ typedef struct
|
||||||
|
|
||||||
double width;
|
double width;
|
||||||
double height;
|
double height;
|
||||||
|
|
||||||
|
double relation_offset_x;
|
||||||
|
double relation_offset_y;
|
||||||
} InvestigationGraphNodeLayout;
|
} InvestigationGraphNodeLayout;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
@ -130,6 +133,58 @@ struct InvestigationGraphView
|
||||||
static const char *investigation_graph_view_get_node_identifier(
|
static const char *investigation_graph_view_get_node_identifier(
|
||||||
const InvestigationGraphNodeLayout *node_layout
|
const InvestigationGraphNodeLayout *node_layout
|
||||||
);
|
);
|
||||||
|
static double investigation_graph_view_get_node_center_x(
|
||||||
|
const InvestigationGraphNodeLayout *node_layout
|
||||||
|
);
|
||||||
|
static double investigation_graph_view_get_node_center_y(
|
||||||
|
const InvestigationGraphNodeLayout *node_layout
|
||||||
|
);
|
||||||
|
|
||||||
|
/** @brief Replace les libellés de relation entre leurs extrémités. */
|
||||||
|
static void investigation_graph_view_sync_relation_layouts(
|
||||||
|
InvestigationGraphView *graph_view
|
||||||
|
)
|
||||||
|
{
|
||||||
|
if (graph_view == NULL || graph_view->node_layouts == NULL ||
|
||||||
|
graph_view->node_layouts_by_identifier == NULL)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (guint i = 0; i < graph_view->node_layouts->len; i++)
|
||||||
|
{
|
||||||
|
InvestigationGraphNodeLayout *relation_layout =
|
||||||
|
g_ptr_array_index(graph_view->node_layouts, i);
|
||||||
|
const RelationRecord *relation_record = NULL;
|
||||||
|
InvestigationGraphNodeLayout *source_layout = NULL;
|
||||||
|
InvestigationGraphNodeLayout *target_layout = NULL;
|
||||||
|
|
||||||
|
if (relation_layout == NULL || relation_layout->kind !=
|
||||||
|
INVESTIGATION_GRAPH_NODE_KIND_RELATION)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
relation_record = relation_layout->relation_record;
|
||||||
|
source_layout = g_hash_table_lookup(
|
||||||
|
graph_view->node_layouts_by_identifier,
|
||||||
|
relation_record_get_source_entity_identifier(relation_record));
|
||||||
|
target_layout = g_hash_table_lookup(
|
||||||
|
graph_view->node_layouts_by_identifier,
|
||||||
|
relation_record_get_target_entity_identifier(relation_record));
|
||||||
|
if (source_layout == NULL || target_layout == NULL)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
relation_layout->x =
|
||||||
|
(investigation_graph_view_get_node_center_x(source_layout) +
|
||||||
|
investigation_graph_view_get_node_center_x(target_layout)) / 2.0 -
|
||||||
|
relation_layout->width / 2.0 + relation_layout->relation_offset_x;
|
||||||
|
relation_layout->y =
|
||||||
|
(investigation_graph_view_get_node_center_y(source_layout) +
|
||||||
|
investigation_graph_view_get_node_center_y(target_layout)) / 2.0 -
|
||||||
|
relation_layout->height / 2.0 + relation_layout->relation_offset_y;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Borne un facteur de zoom aux limites autorisées.
|
* @brief Borne un facteur de zoom aux limites autorisées.
|
||||||
|
|
@ -577,6 +632,8 @@ investigation_graph_view_find_node_at(
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
investigation_graph_view_sync_relation_layouts(graph_view);
|
||||||
|
|
||||||
layout_position =
|
layout_position =
|
||||||
graph_view->node_layouts->len;
|
graph_view->node_layouts->len;
|
||||||
|
|
||||||
|
|
@ -611,6 +668,63 @@ investigation_graph_view_find_node_at(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Rend également l'arête sélectionnable, avec une tolérance visuelle. */
|
||||||
|
for (layout_position = graph_view->node_layouts->len;
|
||||||
|
layout_position > 0; layout_position--)
|
||||||
|
{
|
||||||
|
InvestigationGraphNodeLayout *relation_layout =
|
||||||
|
g_ptr_array_index(graph_view->node_layouts, layout_position - 1U);
|
||||||
|
const RelationRecord *relation_record = NULL;
|
||||||
|
InvestigationGraphNodeLayout *source_layout = NULL;
|
||||||
|
InvestigationGraphNodeLayout *target_layout = NULL;
|
||||||
|
double source_x = 0.0, source_y = 0.0;
|
||||||
|
double target_x = 0.0, target_y = 0.0;
|
||||||
|
double delta_x = 0.0, delta_y = 0.0, length_squared = 0.0;
|
||||||
|
double projection = 0.0, nearest_x = 0.0, nearest_y = 0.0;
|
||||||
|
double distance_x = 0.0, distance_y = 0.0;
|
||||||
|
double tolerance = 7.0 / graph_view->zoom;
|
||||||
|
|
||||||
|
if (relation_layout == NULL || relation_layout->kind !=
|
||||||
|
INVESTIGATION_GRAPH_NODE_KIND_RELATION)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
relation_record = relation_layout->relation_record;
|
||||||
|
source_layout = g_hash_table_lookup(
|
||||||
|
graph_view->node_layouts_by_identifier,
|
||||||
|
relation_record_get_source_entity_identifier(relation_record));
|
||||||
|
target_layout = g_hash_table_lookup(
|
||||||
|
graph_view->node_layouts_by_identifier,
|
||||||
|
relation_record_get_target_entity_identifier(relation_record));
|
||||||
|
if (source_layout == NULL || target_layout == NULL)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
source_x = investigation_graph_view_get_node_center_x(source_layout);
|
||||||
|
source_y = investigation_graph_view_get_node_center_y(source_layout);
|
||||||
|
target_x = investigation_graph_view_get_node_center_x(target_layout);
|
||||||
|
target_y = investigation_graph_view_get_node_center_y(target_layout);
|
||||||
|
delta_x = target_x - source_x;
|
||||||
|
delta_y = target_y - source_y;
|
||||||
|
length_squared = delta_x * delta_x + delta_y * delta_y;
|
||||||
|
if (length_squared <= 0.0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
projection = ((logical_x - source_x) * delta_x +
|
||||||
|
(logical_y - source_y) * delta_y) / length_squared;
|
||||||
|
projection = CLAMP(projection, 0.0, 1.0);
|
||||||
|
nearest_x = source_x + projection * delta_x;
|
||||||
|
nearest_y = source_y + projection * delta_y;
|
||||||
|
distance_x = logical_x - nearest_x;
|
||||||
|
distance_y = logical_y - nearest_y;
|
||||||
|
if (distance_x * distance_x + distance_y * distance_y <=
|
||||||
|
tolerance * tolerance)
|
||||||
|
{
|
||||||
|
return relation_layout;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return NULL;
|
return NULL;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1086,6 +1200,8 @@ static void investigation_graph_view_on_drag_update(
|
||||||
logical_y -
|
logical_y -
|
||||||
graph_view->node_drag_pointer_offset_y;
|
graph_view->node_drag_pointer_offset_y;
|
||||||
|
|
||||||
|
investigation_graph_view_sync_relation_layouts(graph_view);
|
||||||
|
|
||||||
gtk_widget_queue_draw(
|
gtk_widget_queue_draw(
|
||||||
graph_view->drawing_area
|
graph_view->drawing_area
|
||||||
);
|
);
|
||||||
|
|
@ -1216,6 +1332,8 @@ static void investigation_graph_view_on_drag_end(
|
||||||
graph_view->dragged_node->y =
|
graph_view->dragged_node->y =
|
||||||
logical_y -
|
logical_y -
|
||||||
graph_view->node_drag_pointer_offset_y;
|
graph_view->node_drag_pointer_offset_y;
|
||||||
|
|
||||||
|
investigation_graph_view_sync_relation_layouts(graph_view);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -2020,6 +2138,9 @@ static gboolean investigation_graph_view_append_relation_layouts(
|
||||||
parallel_offset
|
parallel_offset
|
||||||
);
|
);
|
||||||
|
|
||||||
|
relation_layout->relation_offset_x =
|
||||||
|
perpendicular_x * parallel_offset;
|
||||||
|
|
||||||
relation_layout->y =
|
relation_layout->y =
|
||||||
(
|
(
|
||||||
(source_center_y + target_center_y) /
|
(source_center_y + target_center_y) /
|
||||||
|
|
@ -2031,6 +2152,9 @@ static gboolean investigation_graph_view_append_relation_layouts(
|
||||||
parallel_offset
|
parallel_offset
|
||||||
);
|
);
|
||||||
|
|
||||||
|
relation_layout->relation_offset_y =
|
||||||
|
perpendicular_y * parallel_offset;
|
||||||
|
|
||||||
g_ptr_array_add(
|
g_ptr_array_add(
|
||||||
node_layouts,
|
node_layouts,
|
||||||
relation_layout
|
relation_layout
|
||||||
|
|
@ -3686,6 +3810,8 @@ static void investigation_graph_view_draw(
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
investigation_graph_view_sync_relation_layouts(graph_view);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Les coordonnées de disposition restent exprimées dans l'espace logique
|
* Les coordonnées de disposition restent exprimées dans l'espace logique
|
||||||
* du graphe. Cairo applique ensuite la navigation de la vue.
|
* du graphe. Cairo applique ensuite la navigation de la vue.
|
||||||
|
|
|
||||||
|
|
@ -67,6 +67,9 @@ struct Workspace
|
||||||
GtkWidget *relation_details_panel;
|
GtkWidget *relation_details_panel;
|
||||||
GtkWidget *relation_details_title_label;
|
GtkWidget *relation_details_title_label;
|
||||||
GtkWidget *relation_details_summary_label;
|
GtkWidget *relation_details_summary_label;
|
||||||
|
GtkWidget *edit_relation_button;
|
||||||
|
GtkWidget *close_relation_details_button;
|
||||||
|
char *selected_relation_identifier;
|
||||||
|
|
||||||
GtkWidget *node_page;
|
GtkWidget *node_page;
|
||||||
|
|
||||||
|
|
@ -119,6 +122,9 @@ struct Workspace
|
||||||
gpointer
|
gpointer
|
||||||
add_relation_user_data;
|
add_relation_user_data;
|
||||||
|
|
||||||
|
WorkspaceEditRelationCallback edit_relation_callback;
|
||||||
|
gpointer edit_relation_user_data;
|
||||||
|
|
||||||
WorkspaceOsintActionCallback
|
WorkspaceOsintActionCallback
|
||||||
osint_action_callback;
|
osint_action_callback;
|
||||||
|
|
||||||
|
|
@ -573,6 +579,20 @@ static const char *workspace_get_integrity_status_text(
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @brief Convertit le statut d'une relation en texte utilisateur. */
|
||||||
|
static const char *workspace_get_relation_status_text(RelationStatus status)
|
||||||
|
{
|
||||||
|
switch (status)
|
||||||
|
{
|
||||||
|
case RELATION_STATUS_ACTIVE: return "Active";
|
||||||
|
case RELATION_STATUS_ARCHIVED: return "Archivée";
|
||||||
|
case RELATION_STATUS_DELETED: return "Supprimée";
|
||||||
|
case RELATION_STATUS_DISPUTED: return "Contestée";
|
||||||
|
case RELATION_STATUS_UNKNOWN:
|
||||||
|
default: return "Inconnu";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Transmet la sélection du graphe au volet de détails.
|
* @brief Transmet la sélection du graphe au volet de détails.
|
||||||
*/
|
*/
|
||||||
|
|
@ -623,6 +643,7 @@ static void workspace_on_graph_node_selected(
|
||||||
&workspace->osint_selection_context,
|
&workspace->osint_selection_context,
|
||||||
osint_selection_context_free
|
osint_selection_context_free
|
||||||
);
|
);
|
||||||
|
g_clear_pointer(&workspace->selected_relation_identifier, g_free);
|
||||||
|
|
||||||
if (entity_record != NULL)
|
if (entity_record != NULL)
|
||||||
{
|
{
|
||||||
|
|
@ -640,6 +661,8 @@ static void workspace_on_graph_node_selected(
|
||||||
|
|
||||||
if (workspace->relation_details_panel != NULL)
|
if (workspace->relation_details_panel != NULL)
|
||||||
{
|
{
|
||||||
|
gtk_widget_set_can_target(workspace->relation_details_panel,
|
||||||
|
relation_record != NULL);
|
||||||
gtk_widget_set_visible(
|
gtk_widget_set_visible(
|
||||||
workspace->relation_details_panel,
|
workspace->relation_details_panel,
|
||||||
relation_record != NULL
|
relation_record != NULL
|
||||||
|
|
@ -650,6 +673,8 @@ static void workspace_on_graph_node_selected(
|
||||||
workspace->relation_details_title_label != NULL &&
|
workspace->relation_details_title_label != NULL &&
|
||||||
workspace->relation_details_summary_label != NULL)
|
workspace->relation_details_summary_label != NULL)
|
||||||
{
|
{
|
||||||
|
workspace->selected_relation_identifier = g_strdup(
|
||||||
|
relation_record_get_identifier(relation_record));
|
||||||
const EntityRecord *source_entity =
|
const EntityRecord *source_entity =
|
||||||
investigation_graph_model_find_entity(
|
investigation_graph_model_find_entity(
|
||||||
workspace->graph_model,
|
workspace->graph_model,
|
||||||
|
|
@ -670,11 +695,21 @@ static void workspace_on_graph_node_selected(
|
||||||
);
|
);
|
||||||
const char *label = relation_record_get_label(relation_record);
|
const char *label = relation_record_get_label(relation_record);
|
||||||
char *summary = g_strdup_printf(
|
char *summary = g_strdup_printf(
|
||||||
"%s → %s\n\nType : %s\nConfiance : %d %%\nUUID : %s",
|
"%s → %s\n\nType : %s\nConfiance : %d %%\nStatut : %s\n"
|
||||||
entity_record_get_value(source_entity),
|
"Justification : %s\nCréée le : %s\nModifiée le : %s\nUUID : %s",
|
||||||
entity_record_get_value(target_entity),
|
source_entity != NULL ? entity_record_get_value(source_entity) :
|
||||||
|
"Source inconnue",
|
||||||
|
target_entity != NULL ? entity_record_get_value(target_entity) :
|
||||||
|
"Cible inconnue",
|
||||||
relation_record_get_relation_type(relation_record),
|
relation_record_get_relation_type(relation_record),
|
||||||
relation_record_get_confidence(relation_record),
|
relation_record_get_confidence(relation_record),
|
||||||
|
workspace_get_relation_status_text(
|
||||||
|
relation_record_get_status(relation_record)),
|
||||||
|
relation_record_get_justification(relation_record) != NULL
|
||||||
|
? relation_record_get_justification(relation_record)
|
||||||
|
: "Non renseignée",
|
||||||
|
relation_record_get_created_at(relation_record),
|
||||||
|
relation_record_get_updated_at(relation_record),
|
||||||
relation_record_get_identifier(relation_record)
|
relation_record_get_identifier(relation_record)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
@ -695,6 +730,31 @@ static void workspace_on_graph_node_selected(
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @brief Relaie la demande de modification de la relation affichée. */
|
||||||
|
static void workspace_on_edit_relation_clicked(GtkButton *button,
|
||||||
|
gpointer user_data)
|
||||||
|
{
|
||||||
|
Workspace *workspace = user_data;
|
||||||
|
(void) button;
|
||||||
|
if (workspace == NULL || workspace->edit_relation_callback == NULL ||
|
||||||
|
workspace->selected_relation_identifier == NULL)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
workspace->edit_relation_callback(workspace->selected_relation_identifier,
|
||||||
|
workspace->edit_relation_user_data);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @brief Ferme le volet de relation et désélectionne le graphe. */
|
||||||
|
static void workspace_on_close_relation_details_clicked(GtkButton *button,
|
||||||
|
gpointer user_data)
|
||||||
|
{
|
||||||
|
Workspace *workspace = user_data;
|
||||||
|
(void) button;
|
||||||
|
if (workspace != NULL)
|
||||||
|
investigation_graph_view_clear_selection(workspace->graph_view);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @brief Relaie la demande d'ajout d'une relation.
|
* @brief Relaie la demande d'ajout d'une relation.
|
||||||
*/
|
*/
|
||||||
|
|
@ -1493,12 +1553,17 @@ Workspace *workspace_new(void)
|
||||||
);
|
);
|
||||||
workspace->relation_details_title_label = gtk_label_new(NULL);
|
workspace->relation_details_title_label = gtk_label_new(NULL);
|
||||||
workspace->relation_details_summary_label = gtk_label_new(NULL);
|
workspace->relation_details_summary_label = gtk_label_new(NULL);
|
||||||
|
workspace->edit_relation_button = gtk_button_new_with_label("Modifier");
|
||||||
|
workspace->close_relation_details_button =
|
||||||
|
gtk_button_new_from_icon_name("window-close-symbolic");
|
||||||
|
|
||||||
if (workspace->graph_view == NULL ||
|
if (workspace->graph_view == NULL ||
|
||||||
workspace->entity_details_panel == NULL ||
|
workspace->entity_details_panel == NULL ||
|
||||||
workspace->relation_details_panel == NULL ||
|
workspace->relation_details_panel == NULL ||
|
||||||
workspace->relation_details_title_label == NULL ||
|
workspace->relation_details_title_label == NULL ||
|
||||||
workspace->relation_details_summary_label == NULL)
|
workspace->relation_details_summary_label == NULL ||
|
||||||
|
workspace->edit_relation_button == NULL ||
|
||||||
|
workspace->close_relation_details_button == NULL)
|
||||||
{
|
{
|
||||||
workspace_free(
|
workspace_free(
|
||||||
workspace
|
workspace
|
||||||
|
|
@ -1514,6 +1579,8 @@ Workspace *workspace_new(void)
|
||||||
gtk_widget_set_margin_top(workspace->relation_details_panel, 56);
|
gtk_widget_set_margin_top(workspace->relation_details_panel, 56);
|
||||||
gtk_widget_set_margin_start(workspace->relation_details_panel, 12);
|
gtk_widget_set_margin_start(workspace->relation_details_panel, 12);
|
||||||
gtk_widget_set_margin_bottom(workspace->relation_details_panel, 12);
|
gtk_widget_set_margin_bottom(workspace->relation_details_panel, 12);
|
||||||
|
/* `view` fournit le fond opaque du thème, comme le volet d'entité. */
|
||||||
|
gtk_widget_add_css_class(workspace->relation_details_panel, "view");
|
||||||
gtk_widget_add_css_class(workspace->relation_details_panel, "card");
|
gtk_widget_add_css_class(workspace->relation_details_panel, "card");
|
||||||
gtk_widget_set_margin_start(workspace->relation_details_title_label, 16);
|
gtk_widget_set_margin_start(workspace->relation_details_title_label, 16);
|
||||||
gtk_widget_set_margin_end(workspace->relation_details_title_label, 16);
|
gtk_widget_set_margin_end(workspace->relation_details_title_label, 16);
|
||||||
|
|
@ -1545,11 +1612,23 @@ Workspace *workspace_new(void)
|
||||||
GTK_BOX(workspace->relation_details_panel),
|
GTK_BOX(workspace->relation_details_panel),
|
||||||
workspace->relation_details_title_label
|
workspace->relation_details_title_label
|
||||||
);
|
);
|
||||||
|
gtk_widget_set_tooltip_text(workspace->close_relation_details_button,
|
||||||
|
"Fermer le volet");
|
||||||
|
gtk_widget_add_css_class(workspace->close_relation_details_button, "flat");
|
||||||
|
gtk_box_append(GTK_BOX(workspace->relation_details_panel),
|
||||||
|
workspace->close_relation_details_button);
|
||||||
gtk_box_append(
|
gtk_box_append(
|
||||||
GTK_BOX(workspace->relation_details_panel),
|
GTK_BOX(workspace->relation_details_panel),
|
||||||
workspace->relation_details_summary_label
|
workspace->relation_details_summary_label
|
||||||
);
|
);
|
||||||
|
gtk_box_append(GTK_BOX(workspace->relation_details_panel),
|
||||||
|
workspace->edit_relation_button);
|
||||||
|
g_signal_connect(workspace->edit_relation_button, "clicked",
|
||||||
|
G_CALLBACK(workspace_on_edit_relation_clicked), workspace);
|
||||||
|
g_signal_connect(workspace->close_relation_details_button, "clicked",
|
||||||
|
G_CALLBACK(workspace_on_close_relation_details_clicked), workspace);
|
||||||
gtk_widget_set_visible(workspace->relation_details_panel, FALSE);
|
gtk_widget_set_visible(workspace->relation_details_panel, FALSE);
|
||||||
|
gtk_widget_set_can_target(workspace->relation_details_panel, FALSE);
|
||||||
|
|
||||||
investigation_graph_view_set_selection_callback(
|
investigation_graph_view_set_selection_callback(
|
||||||
workspace->graph_view,
|
workspace->graph_view,
|
||||||
|
|
@ -2906,6 +2985,14 @@ void workspace_set_add_relation_callback(
|
||||||
user_data;
|
user_data;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void workspace_set_edit_relation_callback(Workspace *workspace,
|
||||||
|
WorkspaceEditRelationCallback callback, gpointer user_data)
|
||||||
|
{
|
||||||
|
if (workspace == NULL) return;
|
||||||
|
workspace->edit_relation_callback = callback;
|
||||||
|
workspace->edit_relation_user_data = user_data;
|
||||||
|
}
|
||||||
|
|
||||||
void workspace_reset_graph_layout(
|
void workspace_reset_graph_layout(
|
||||||
Workspace *workspace
|
Workspace *workspace
|
||||||
)
|
)
|
||||||
|
|
@ -2942,6 +3029,7 @@ void workspace_free(Workspace *workspace)
|
||||||
&workspace->osint_selection_context,
|
&workspace->osint_selection_context,
|
||||||
osint_selection_context_free
|
osint_selection_context_free
|
||||||
);
|
);
|
||||||
|
g_clear_pointer(&workspace->selected_relation_identifier, g_free);
|
||||||
|
|
||||||
g_clear_pointer(
|
g_clear_pointer(
|
||||||
&workspace->osint_action_catalog,
|
&workspace->osint_action_catalog,
|
||||||
|
|
|
||||||
|
|
@ -2225,6 +2225,47 @@ static void test_relation_dao_foreign_key_restrict(void)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** @brief Vérifie la modification des attributs sans changer les extrémités. */
|
||||||
|
static void test_relation_dao_update_existing(void)
|
||||||
|
{
|
||||||
|
static const char identifier[] =
|
||||||
|
"90000000-0000-4000-8000-000000000001";
|
||||||
|
TestRelationDaoFixture fixture = test_relation_dao_fixture_create();
|
||||||
|
RelationRecord *initial = NULL;
|
||||||
|
RelationRecord *updated = NULL;
|
||||||
|
RelationRecord *stored = NULL;
|
||||||
|
GError *error = NULL;
|
||||||
|
|
||||||
|
test_relation_dao_insert_standard_entities(&fixture);
|
||||||
|
initial = test_relation_dao_create_relation(identifier,
|
||||||
|
TEST_ENTITY_A_IDENTIFIER, TEST_ENTITY_B_IDENTIFIER, "uses",
|
||||||
|
"Ancien libellé", "Ancienne justification", 40,
|
||||||
|
"2026-07-20T20:00:00Z", "2026-07-20T20:00:00Z",
|
||||||
|
RELATION_STATUS_ACTIVE);
|
||||||
|
test_relation_dao_insert_relation(&fixture, initial);
|
||||||
|
updated = test_relation_dao_create_relation(identifier,
|
||||||
|
TEST_ENTITY_A_IDENTIFIER, TEST_ENTITY_B_IDENTIFIER, "controls",
|
||||||
|
"Nouveau libellé", "Nouvelle justification", 90,
|
||||||
|
"2026-07-20T20:00:00Z", "2026-07-22T16:00:00Z",
|
||||||
|
RELATION_STATUS_ACTIVE);
|
||||||
|
assert(relation_dao_update(fixture.relation_dao, updated, &error));
|
||||||
|
assert(error == NULL);
|
||||||
|
stored = relation_dao_find_by_identifier(fixture.relation_dao,
|
||||||
|
identifier, &error);
|
||||||
|
assert(stored != NULL && error == NULL);
|
||||||
|
assert(strcmp(relation_record_get_relation_type(stored), "controls") == 0);
|
||||||
|
assert(strcmp(relation_record_get_label(stored), "Nouveau libellé") == 0);
|
||||||
|
assert(relation_record_get_confidence(stored) == 90);
|
||||||
|
assert(strcmp(relation_record_get_source_entity_identifier(stored),
|
||||||
|
TEST_ENTITY_A_IDENTIFIER) == 0);
|
||||||
|
assert(strcmp(relation_record_get_created_at(stored),
|
||||||
|
"2026-07-20T20:00:00Z") == 0);
|
||||||
|
relation_record_free(stored);
|
||||||
|
relation_record_free(updated);
|
||||||
|
relation_record_free(initial);
|
||||||
|
test_relation_dao_fixture_clear(&fixture);
|
||||||
|
}
|
||||||
|
|
||||||
int main(
|
int main(
|
||||||
int argc,
|
int argc,
|
||||||
char **argv
|
char **argv
|
||||||
|
|
@ -2256,6 +2297,7 @@ int main(
|
||||||
test_relation_dao_missing_table();
|
test_relation_dao_missing_table();
|
||||||
test_relation_dao_reflexive_schema_constraint();
|
test_relation_dao_reflexive_schema_constraint();
|
||||||
test_relation_dao_foreign_key_restrict();
|
test_relation_dao_foreign_key_restrict();
|
||||||
|
test_relation_dao_update_existing();
|
||||||
|
|
||||||
printf(
|
printf(
|
||||||
"RelationDao : tous les tests sont valides.\n"
|
"RelationDao : tous les tests sont valides.\n"
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue