diff --git a/app/fr/maif/izanami/datastores/ImportExportDatastore.scala b/app/fr/maif/izanami/datastores/ImportExportDatastore.scala index 5bc60ae33..01f829823 100644 --- a/app/fr/maif/izanami/datastores/ImportExportDatastore.scala +++ b/app/fr/maif/izanami/datastores/ImportExportDatastore.scala @@ -4,7 +4,7 @@ import fr.maif.izanami.datastores.ImportExportDatastore.TableMetadata import fr.maif.izanami.env.Env import fr.maif.izanami.env.pgimplicits.EnhancedRow import fr.maif.izanami.errors.{InternalServerError, IzanamiError, PartialImportFailure} -import fr.maif.izanami.models.{ExportedType, KeyRightType, ProjectRightType, WebhookRightType} +import fr.maif.izanami.models.{ExportedType, KeyRightType, ProjectRightType, TenantCreationRequest, WebhookRightType} import fr.maif.izanami.models.ExportedType.exportedTypeToString import fr.maif.izanami.utils.Datastore import fr.maif.izanami.utils.syntax.implicits.BetterJsValue @@ -13,7 +13,7 @@ import fr.maif.izanami.web.ImportController.ImportConflictStrategy import fr.maif.izanami.web.{ExportController, ImportController} import io.vertx.core.json.JsonArray import io.vertx.sqlclient.SqlConnection -import play.api.libs.json.{JsObject, Json} +import play.api.libs.json.{JsObject, JsString, Json} import java.util.concurrent.atomic.AtomicInteger import scala.concurrent.Future @@ -47,8 +47,10 @@ class ImportExportDatastore(val env: Env) extends Datastore { def importTenantData( tenant: String, + user: String, entries: Map[ExportedType, Seq[JsObject]], - conflictStrategy: ImportConflictStrategy + conflictStrategy: ImportConflictStrategy, + wipeData: Boolean ): Future[Either[IzanamiError, Unit]] = { Future @@ -70,38 +72,55 @@ class ImportExportDatastore(val env: Env) extends Datastore { .foldLeft(Future.successful(Right(())): Future[Either[Map[ExportedType, Seq[JsObject]], Unit]])( (agg, t) => { agg.flatMap(previousResult => { - val f = conflictStrategy match { - case ImportController.MergeOverwrite => importTenantDataWithMergeOnConflict(tenant, t._1, t._2) - case ImportController.Skip => importTenantDataWithSkipOnConflict(tenant, t._1, t._2) - case ImportController.Fail if previousResult.isRight => - importTenantDataWithFailOnConflict(tenant, t._1, t._2, conn) - case _ => Future.successful(Right(())) - } + if(wipeData) { + env.datastores.tenants.deleteTenant(tenant, user).flatMap { + case Left(err) => Future.successful(Left(Map(t._3 -> Seq(JsObject(Seq("error" -> JsString(err.toString))))))) + case Right(_) => + env.datastores.tenants.createTenant(TenantCreationRequest(tenant), user).flatMap { + case Left(err) => Future.successful(Left(Map(t._3 -> Seq(JsObject(Seq("error" -> JsString(err.toString))))))) + case Right(_) => importTenantDataWithSkipOnConflict(tenant, t._1, t._2).map { + case Left(failedJsons) => + Left(Map(t._3 -> failedJsons)) + case Right(_) => + Right(()) + } + } + } + } else { + val f = conflictStrategy match { + case ImportController.MergeOverwrite => importTenantDataWithMergeOnConflict(tenant, t._1, t._2) + case ImportController.Skip => importTenantDataWithSkipOnConflict(tenant, t._1, t._2) + case ImportController.Fail if previousResult.isRight => + importTenantDataWithFailOnConflict(tenant, t._1, t._2, conn) + case _ => Future.successful(Right(())) - f - .flatMap(e => { - updateUserTenantRightIfNeeded( - tenant, - entries, - if (conflictStrategy == ImportController.Fail) Some(conn) else None - ) - .map(_ => e) - }) - .map(either => - either.left.map(jsons => - jsons.map(json => Json.obj("row" -> json, "_type" -> exportedTypeToString(t._3))) + } + f + .flatMap(e => { + updateUserTenantRightIfNeeded( + tenant, + entries, + if (conflictStrategy == ImportController.Fail) Some(conn) else None + ) + .map(_ => e) + }) + .map(either => + either.left.map(jsons => + jsons.map(json => Json.obj("row" -> json, "_type" -> exportedTypeToString(t._3))) + ) ) - ) - .map(either => - (either, previousResult) match { - case (Left(err), Left(errs)) => { - Left(errs + (t._3 -> err)) + .map(either => + (either, previousResult) match { + case (Left(err), Left(errs)) => { + Left(errs + (t._3 -> err)) + } + case (Left(err), Right(_)) => Left(Map(t._3 -> err)) + case (Right(_), Right(_)) => Right(()) + case (Right(_), Left(errs)) => Left(errs) } - case (Left(err), Right(_)) => Left(Map(t._3 -> err)) - case (Right(_), Right(_)) => Right(()) - case (Right(_), Left(errs)) => Left(errs) - } - ) + ) + } + }) } ) @@ -110,7 +129,6 @@ class ImportExportDatastore(val env: Env) extends Datastore { } }) } - def importTenantDataWithMergeOnConflict( tenant: String, metadata: TableMetadata, diff --git a/app/fr/maif/izanami/web/ImportController.scala b/app/fr/maif/izanami/web/ImportController.scala index c7e4510c4..0a6e3690b 100644 --- a/app/fr/maif/izanami/web/ImportController.scala +++ b/app/fr/maif/izanami/web/ImportController.scala @@ -194,6 +194,7 @@ class ImportController( version: Int, tenant: String, conflict: String, + wipeData: Option[Boolean], timezone: Option[String], deduceProject: Boolean, create: Option[Boolean], @@ -219,7 +220,7 @@ class ImportController( }) .getOrElse(BadRequest(Json.obj("message" -> "Missing timezone")).future) } else if (version == 2) { - importV2Data(request, tenant, conflict) + importV2Data(request, tenant, conflict, wipeData) } else { BadRequest(Json.obj("message" -> s"Invalid version: $version")).future } @@ -228,12 +229,14 @@ class ImportController( def importV2Data( request: UserNameRequest[MultipartFormData[Files.TemporaryFile]], tenant: String, - conflict: String + conflict: String, + wipeData: Option[Boolean] ): Future[Result] = { val files: Map[String, URI] = request.body.files.map(f => (f.key, f.ref.path.toUri)).toMap (for ( conflictStrategy <- ImportController.parseStrategy(conflict); - uri <- files.get("export") + uri <- files.get("export"); + wipeData <- wipeData ) yield { Try(scala.io.Source.fromFile(uri)).toEither.left .map(ex => { @@ -258,19 +261,20 @@ class ImportController( .map(m => fixImportDataIfNeeded(tenant, m)) .map { case (messages, data) => { - env.datastores.exportDatastore - .importTenantData(tenant, data, conflictStrategy) - .map { - case Left(PartialImportFailure(failedElements)) => - Conflict( - Json.obj( - "messages" -> Json.toJson(messages), - "conflicts" -> Json.toJson(failedElements.values.flatten) + env.datastores.exportDatastore + .importTenantData(tenant, request.user, data, conflictStrategy, wipeData) + .map { + case Left(PartialImportFailure(failedElements)) => + Conflict( + Json.obj( + "messages" -> Json.toJson(messages), + "conflicts" -> Json.toJson(failedElements.values.flatten) + ) ) - ) - case Right(_) => Ok(Json.obj("messages" -> Json.toJson(messages))) - case Left(err) => err.toHttpResponse - } + case Right(_) => Ok(Json.obj("messages" -> Json.toJson(messages))) + case Left(err) => err.toHttpResponse + } + } } }).toRight(Future.successful(BadRequest(Json.obj("message" -> "Missing export file")))).flatten.fold(r => r, r => r) diff --git a/conf/routes b/conf/routes index c0dacd34a..2a0978306 100644 --- a/conf/routes +++ b/conf/routes @@ -96,7 +96,7 @@ PUT /api/admin/tenants/:tenant/local-scripts/:script DELETE /api/admin/local-scripts/_cache fr.maif.izanami.web.PluginController.clearWasmCache() DELETE /api/admin/tenants/:tenant/local-scripts/:script fr.maif.izanami.web.PluginController.deleteScript(tenant: String, script: String) -POST /api/admin/tenants/:tenant/_import fr.maif.izanami.web.ImportController.importData(version: Int, tenant: String, conflict: String, timezone: Option[String], deduceProject: Boolean ?= false, create: Option[Boolean], project: Option[String], projectPartSize: Option[Int], inlineScript: Option[Boolean]) +POST /api/admin/tenants/:tenant/_import fr.maif.izanami.web.ImportController.importData(version: Int, tenant: String, conflict: String,wipeData : Option[Boolean], timezone: Option[String], deduceProject: Boolean ?= false, create: Option[Boolean], project: Option[String], projectPartSize: Option[Int], inlineScript: Option[Boolean]) GET /api/admin/tenants/:tenant/_import/:id fr.maif.izanami.web.ImportController.readImportStatus(tenant: String, id: String) DELETE /api/admin/tenants/:tenant/_import/:id fr.maif.izanami.web.ImportController.deleteImportStatus(tenant: String, id: String) diff --git a/izanami-frontend/src/pages/tenantSettings.tsx b/izanami-frontend/src/pages/tenantSettings.tsx index f2fd78737..7eb758e15 100644 --- a/izanami-frontend/src/pages/tenantSettings.tsx +++ b/izanami-frontend/src/pages/tenantSettings.tsx @@ -85,17 +85,23 @@ export function TenantSettings(props: { tenant: string }) { const navigate = useNavigate(); const formTitleRef = React.useRef(null); + const formExportTitleRef = React.useRef(null); const [modification, setModification] = React.useState(false); if (tenantQuery.isLoading || usersQuery.isLoading) { return ; } + if (tenantQuery.isError || usersQuery.isError) { + return
Failed to fetch tenant / tenant users
; + } if (tenantQuery.isSuccess && usersQuery.isSuccess) { return ( <> -

Settings for tenant {tenant}

-

- Tenant users +

Tenant settings

+
+

Manage permissions

+

+ Users -

+ {inviting && ( @@ -117,9 +123,9 @@ export function TenantSettings(props: { tenant: string }) { )}
-

Update tenant information

+

General settings

- Update description for this tenant + Update description for this tenant: {tenant}

-

Export / import data

+

Import data

{v1ImportDisplayed ? ( <>

Import data from Izanami v1 instance

@@ -228,17 +234,6 @@ export function TenantSettings(props: { tenant: string }) { }} /> - ) : exportDisplayed ? ( -
- setExportDisplayed(false)} - submit={(request: IzanamiTenantExportRequest) => - requestExport(tenant, request).then(() => - setExportDisplayed(false) - ) - } - /> -
) : importDisplayed ? (
- Import data from V1 Izanami instance + Import data from 1.x Izanami instance
@@ -304,27 +299,40 @@ export function TenantSettings(props: { tenant: string }) { Import data
-
- Export data to transfer them to another instance - -
)} +
+

Export Data

+ {!exportDisplayed ? ( +
+ Export data to transfer them to another instance + +
+ ) : ( +
+ setExportDisplayed(false)} + submit={(request: IzanamiTenantExportRequest) => + requestExport(tenant, request).then(() => + setExportDisplayed(false) + ) + } + /> +
+ )} ); - } else { - return
Failed to fetch tenant / tenant users
; } } @@ -400,52 +408,68 @@ function ImportForm(props: { const { cancel, submit } = props; const methods = useForm({ - defaultValues: {}, + defaultValues: { wipeData: false }, }); const { handleSubmit, register, + getValues, + watch, control, formState: { isSubmitting }, } = methods; - + watch(["wipeData"]); return (
submit(data))} > -

Import data

+ {!getValues("wipeData") && ( +