feat(Codex): edit archives

This commit is contained in:
2026-01-15 17:52:47 +01:00
parent 6cf5262dd5
commit ec109328fb
6 changed files with 650 additions and 195 deletions

View File

@@ -5,6 +5,7 @@ open Fable.Core
open Fable.Core.JsInterop
open Feliz
open Feliz.Router
open FS.FluentUI
module Archive =
let private fetchArchiveAcl (id: System.Guid) : JS.Promise<Result<Archmaester.Dto.ArchiveAcl, string>> =
@@ -29,16 +30,6 @@ module Archive =
return subs
}
let private deleteArchive (id: System.Guid) =
promise {
try
let! res = Remoting.adminApi.deleteArchive id |> Async.StartAsPromise
return res
with e ->
console.error("Error deleting archive: %o", e)
return Error "Error deleting archive"
}
let private addArchiveGroup (archiveId: System.Guid) (group: string) =
promise {
try
@@ -227,9 +218,6 @@ module Archive =
let View (archiveId: System.Guid) =
let loading, setLoading = React.useState true
let error, setError = React.useState<string option> None
let editing, setEditing = React.useState false
let deleting, setDeleting = React.useState false
let deleted, setDeleted = React.useState false
let selectedGroup, setSelectedGroup = React.useState<string option> None
let archiveOpt, setArchive = React.useState<Archmaester.Dto.ArchiveProps option> None
let aclOpt, setAcl = React.useState<Archmaester.Dto.ArchiveAcl option> None
@@ -257,21 +245,6 @@ module Archive =
| None ->
console.warn("ACL has not been downloaded")
let handleDeleteArchive (id: System.Guid) =
deleteArchive id
|> Promise.iter (fun res ->
match res with
| Ok deleted ->
if deleted then
console.info("Archive deleted successfully")
setDeleted true
else
setError (Some "Failed to delete archive")
| Error err ->
console.error("Error deleting archive: %s", err)
setError (Some err)
)
let handleSelectedGroupChange (groupOpt: string option) =
console.debug("Selected group: %s", groupOpt)
setSelectedGroup groupOpt
@@ -319,94 +292,27 @@ module Archive =
| None ->
match archiveOpt with
| Some archive ->
Html.h1 (sprintf "Archive %s" archive.name)
Fui.text.title1 (sprintf "Archive %s" archive.name)
if deleting then
Html.h2 "Deleting archive ..."
else
Html.none
if deleted then
Html.div [
prop.children [
Html.h2 "Archive successfully deleted"
Html.a [
prop.href (Router.format "archives")
prop.text "Return to archives listing"
]
]
Html.div [
prop.classes [ "flex-row"; "gap-8" ]
prop.children [
Archives.EditArchiveDialog archive (fun edited ->
Some
{archive with
name = edited.Name
startTime = edited.StartTime
endTime = edited.StartTime.AddHours(edited.Frames)
frames = edited.Frames
isPublished = edited.Published
isPublic = edited.Public
}
|> setArchive
console.debug ("response: ", edited)
)
Archives.DeleteArchiveDialog archive
]
else
Html.div [
prop.classes [ "flex-row"; "gap-8" ]
prop.children [
if deleting then
Html.button [
prop.onClick (fun ev ->
setDeleting false
handleDeleteArchive archive.archiveId
)
prop.text "Save"
]
Html.button [
prop.onClick (fun ev ->
setDeleting false
)
prop.text "Cancel"
]
elif editing then
Html.button [
prop.onClick (fun ev ->
setEditing false
)
prop.text "Save"
]
Html.button [
prop.onClick (fun ev ->
setEditing false
)
prop.text "Cancel"
]
else
Html.button [
prop.onClick (fun ev ->
setEditing true
)
prop.text "Edit"
]
Html.button [
prop.onClick (fun ev ->
setDeleting true
)
prop.text "Delete"
]
]
]
if editing then
Html.form [
prop.children [
Html.div [
prop.children [
Html.label [
prop.htmlFor "published-checkbox"
prop.text "Published: "
]
Html.input [
prop.id "published-checkbox"
prop.type' "checkbox"
prop.custom ("checked", archive.isPublished)
]
]
]
]
]
else
Html.none
]
Archives.InfoSection archive
@@ -744,4 +650,4 @@ module Archive =
| None ->
Html.h1 "Archive not found"
]
]

View File

@@ -1,6 +1,8 @@
namespace Oceanbox.Codex
open Fable.Core
open Feliz
open FS.FluentUI
type Archives =
[<ReactComponent>]
@@ -13,49 +15,131 @@ type Archives =
Html.section [
prop.classes [ "flex-row"; "flex-wrap"; "gap-16" ]
prop.children [
Html.ul [
prop.children [
Html.li [
prop.text (sprintf "Description: %s" archive.description)
]
Html.li [
prop.text (sprintf "Archive type: %s" (string archive.archiveType))
]
Html.li [
prop.text (sprintf "Projection: %s" archive.projection)
]
Html.li [
prop.text (sprintf "Frequency: %d" archive.freq)
]
Html.li [
prop.text (sprintf "Frames: %d" archive.frames)
]
Html.li [
prop.text (sprintf "Created: %s" (archive.created.ToLongDateString()))
]
Html.li [
prop.text (sprintf "Start time: %s" (archive.startTime.ToLongDateString()))
]
Html.li [
prop.text (sprintf "End time: %s" (archive.endTime.ToLongDateString()))
]
Html.li [
prop.text (sprintf "Length: %d days %d hours" archiveLength.Days archiveLength.Hours)
]
Html.li [
prop.text (sprintf "Owner: %s" archive.owner)
]
Html.li [
prop.text (sprintf "Expires: %s" (archive.expires |> Option.map string |> Option.defaultValue ""))
]
Html.li [
prop.text (sprintf "Publised: %b" archive.isPublished)
]
Html.li [
prop.text (sprintf "Public: %b" archive.isPublic)
]
Html.li [
prop.text (sprintf "Location: %s" "tos")
Fui.table [
table.classes [ "flex-basis-7"; "flex-grow" ]
table.children [
Fui.tableBody [
Fui.tableRow [
tableRow.children [
Fui.tableCell [
Fui.tableCellLayout [
tableCellLayout.media (Fui.icon.textDescriptionRegular [])
tableCellLayout.children [ Fui.text "Description" ]
]
]
Fui.tableCell [ Fui.text archive.description ]
]
]
Fui.tableRow [
tableRow.children [
Fui.tableCell [
Fui.tableCellLayout [
tableCellLayout.media (Fui.icon.contactCardGenericRegular [])
tableCellLayout.children [ Fui.text "Archive type" ]
]
]
Fui.tableCell [ Fui.text (string archive.archiveType) ]
]
]
Fui.tableRow [
tableRow.children [
Fui.tableCell [
Fui.tableCellLayout [
tableCellLayout.media (Fui.icon.globeSurfaceRegular [])
tableCellLayout.children [ Fui.text "Projection" ]
]
]
Fui.tableCell [ Fui.text archive.projection ]
]
]
Fui.tableRow [
tableRow.children [
Fui.tableCell [
Fui.tableCellLayout [
tableCellLayout.media (Fui.icon.timerRegular [])
tableCellLayout.children [ Fui.text "Frequency" ]
]
]
Fui.tableCell [ Fui.text (string archive.freq) ]
]
]
Fui.tableRow [
tableRow.children [
Fui.tableCell [
Fui.tableCellLayout [
tableCellLayout.media (Fui.icon.filmstripRegular [])
tableCellLayout.children [ Fui.text "Frames" ]
]
]
Fui.tableCell [ Fui.text (string archive.frames) ]
]
]
Fui.tableRow [
tableRow.children [
Fui.tableCell [
Fui.tableCellLayout [
tableCellLayout.media (Fui.icon.calendarAddRegular [])
tableCellLayout.children [ Fui.text "Time created" ]
]
]
Fui.tableCell [ Fui.text (archive.created.ToLongDateString()) ]
]
]
Fui.tableRow [
tableRow.children [
Fui.tableCell [
Fui.tableCellLayout [
tableCellLayout.media (Fui.icon.playRegular [])
tableCellLayout.children [ Fui.text "Start time" ]
]
]
Fui.tableCell [ Fui.text (archive.startTime.ToLongDateString()) ]
]
]
Fui.tableRow [
tableRow.children [
Fui.tableCell [
Fui.tableCellLayout [
tableCellLayout.media (Fui.icon.stopRegular [])
tableCellLayout.children [ Fui.text "End time" ]
]
]
Fui.tableCell [ Fui.text (archive.endTime.ToLongDateString()) ]
]
]
Fui.tableRow [
tableRow.children [
Fui.tableCell [
Fui.tableCellLayout [
tableCellLayout.media (Fui.icon.autoFitWidthRegular [])
tableCellLayout.children [ Fui.text "Length" ]
]
]
Fui.tableCell [ Fui.text (sprintf "%d days %d hours" archiveLength.Days archiveLength.Hours) ]
]
]
Fui.tableRow [
tableRow.children [
Fui.tableCell [
Fui.tableCellLayout [
tableCellLayout.media (Fui.icon.eyeRegular [])
tableCellLayout.children [ Fui.text "Published" ]
]
]
Fui.tableCell [ Fui.text (string archive.isPublished) ]
]
]
Fui.tableRow [
tableRow.children [
Fui.tableCell [
Fui.tableCellLayout [
tableCellLayout.media (Fui.icon.peopleEyeRegular [])
tableCellLayout.children [ Fui.text "Public" ]
]
]
Fui.tableCell [ Fui.text (string archive.isPublic) ]
]
]
]
]
]
@@ -70,3 +154,309 @@ type Archives =
]
]
]
[<ReactComponent>]
static member EditArchiveDialog(archive: Archmaester.Dto.ArchiveProps) onEdit =
let initForm : Remoting.EditArchiveRequest =
{
Name = archive.name
StartTime = archive.startTime
Frames = archive.frames
Published = archive.isPublished
Public = archive.isPublic
}
let form, setForm = React.useState initForm
let isOpen, setIsOpen = React.useState false
let errMsg, setErrMsg = React.useState None
let handleEditArchive () =
let utcTime = System.DateTime(form.StartTime.Year, form.StartTime.Month, form.StartTime.Day, 0, 0, 0, System.DateTimeKind.Utc)
Remoting.adminApi.updateArchive archive.archiveId {form with StartTime = utcTime}
|> Async.StartAsPromise
|> Promise.iter (fun res ->
match res with
| Ok newArchive ->
Browser.Dom.console.info("Added archive %s with id %s", newArchive.Name, string newArchive.Id)
onEdit newArchive
setIsOpen false
| Error msg ->
Browser.Dom.console.error("Error adding archive %s: %s", form.Name, msg)
setErrMsg (Some msg)
)
Fui.dialog [
dialog.open' isOpen
dialog.onOpenChange (fun (d: DialogOpenChangeData<Browser.Types.MouseEvent>) ->
setIsOpen d.``open``
// NOTE: Reset form on open, so it's not noticeable in the UI
if d.``open`` then
setForm initForm
)
dialog.children [
Fui.dialogTrigger [
dialogTrigger.children (
Fui.button [
button.icon (Fui.icon.editRegular [])
button.text "Edit"
]
)
]
Fui.dialogSurface [
dialogSurface.classes []
dialogSurface.children [
Fui.dialogBody [
dialogBody.classes []
dialogBody.children [
Fui.dialogTitle [
dialogTitle.text (sprintf "Edit archive %s" archive.name)
dialogTitle.action (
Fui.dialogTrigger [
dialogTrigger.action.close
dialogTrigger.children (
Fui.button [
button.appearance.transparent
button.icon (Fui.icon.dismissRegular [])
]
)
]
)
]
Fui.dialogContent [
dialogContent.classes ["flex-column"; "gap-8"]
dialogContent.children [
Fui.field [
field.label (
Fui.label [
label.required true
label.text "Name"
]
)
field.children (
Fui.input [
input.value form.Name
input.onChange (fun (v: ValueProp<string>) ->
if v.value.Length <= 80 then
setForm {form with Name = v.value}
)
]
)
field.hint $"{form.Name.Length}/80"
]
Html.div [
prop.classes ["flex-row"; "gap-8"]
prop.children [
Fui.field [
field.label (
Fui.label [
label.required true
label.text "Start Date"
]
)
field.children (
Fui.datePicker [
datePicker.placeholder "Select a date..."
datePicker.showWeekNumbers true
datePicker.formatDate (fun d -> d.ToShortDateString())
datePicker.value (Some form.StartTime)
datePicker.onSelectDate (fun d ->
d |> Option.iter (fun d' ->
setForm {form with StartTime = d'}
)
)
]
)
]
Fui.field [
field.label (
Fui.label [
label.required true
label.text "Days"
]
)
field.children (
Fui.spinButton [
spinButton.value (form.Frames / 24)
spinButton.min 1
spinButton.onChange (fun (d: SpinButtonOnChangeData) ->
match d.value with
| Some v ->
setForm {form with Frames = v * 24}
| None ->
if d.displayValue.ToCharArray() |> Array.forall System.Char.IsDigit then
setForm {form with Frames = int d.displayValue * 24}
)
]
)
]
]
]
Fui.text.caption1 [
let endDate = form.StartTime.AddHours(form.Frames).ToShortDateString()
text.text (sprintf "End Date: %s, %d frames" endDate form.Frames)
]
Fui.checkbox [
checkbox.label "Published"
checkbox.checked' form.Published
checkbox.onCheckedChange (fun c ->
if not c then
setForm {form with Published = c; Public = c}
else
setForm {form with Published = c}
)
]
Fui.checkbox [
checkbox.label "Public"
checkbox.checked' form.Public
checkbox.onCheckedChange (fun c -> setForm {form with Public = c})
]
match errMsg with
| None -> Html.none
| Some msg ->
Fui.text [
text.style [style.color Theme.tokens.colorStatusDangerForeground1]
text.text msg
]
]
]
Fui.dialogActions [
dialogActions.position.end'
dialogActions.children [
Fui.dialogTrigger [
dialogTrigger.action.close
dialogTrigger.children (
Fui.button [
button.icon (Fui.icon.dismissRegular [])
button.text "Cancel"
]
)
]
Fui.button [
button.appearance.primary
button.icon (Fui.icon.saveRegular [])
button.text "Save changes"
button.disabled <| (form = initForm)
button.onClick (fun _ -> handleEditArchive ())
]
]
]
]
]
]
]
]
]
[<ReactComponent>]
static member DeleteArchiveDialog(archive: Archmaester.Dto.ArchiveProps) =
let isOpen, setIsOpen = React.useState false
let userConfirmed, setUserConfirmed = React.useState false
let errMsg, setErrMsg = React.useState None
let deleteArchive (id: System.Guid) =
promise {
try
let! res = Remoting.adminApi.deleteArchive id |> Async.StartAsPromise
return res
with e ->
Browser.Dom.console.error("Error deleting archive: %o", e)
return Error "Error deleting archive"
}
let handleDeleteArchive () =
deleteArchive archive.archiveId
|> Promise.iter (fun res ->
match res with
| Ok deleted ->
if deleted then
Browser.Dom.console.info("Archive deleted successfully")
setIsOpen false
Router.Router.navigateBack ()
else
setErrMsg (Some "Failed to delete archive")
| Error err ->
Browser.Dom.console.error("Error deleting archive: %s", err)
setErrMsg (Some err)
)
Fui.dialog [
dialog.open' isOpen
dialog.onOpenChange (fun (d: DialogOpenChangeData<Browser.Types.MouseEvent>) ->
setIsOpen d.``open``
if d.``open`` then
setUserConfirmed false
setErrMsg None
)
dialog.children [
Fui.dialogTrigger [
dialogTrigger.children (
Fui.button [
button.icon (Fui.icon.deleteRegular [])
button.text "Delete"
]
)
]
Fui.dialogSurface [
dialogSurface.classes []
dialogSurface.children [
Fui.dialogBody [
dialogBody.classes []
dialogBody.children [
Fui.dialogTitle [
dialogTitle.text (sprintf "Delete archive %s" archive.name)
dialogTitle.action (
Fui.dialogTrigger [
dialogTrigger.action.close
dialogTrigger.children (
Fui.button [
button.appearance.transparent
button.icon (Fui.icon.dismissRegular [])
]
)
]
)
]
Fui.dialogContent [
dialogContent.classes ["flex-column"; "gap-8"]
dialogContent.children [
Fui.text "Are you sure you want to delete the archive? This action cannot be reverted!"
Fui.checkbox [
checkbox.label "Yes, I am sure"
checkbox.checked' userConfirmed
checkbox.onCheckedChange setUserConfirmed
]
match errMsg with
| None -> Html.none
| Some msg ->
Fui.text [
text.style [style.color Theme.tokens.colorStatusDangerForeground1]
text.text msg
]
]
]
Fui.dialogActions [
dialogActions.position.end'
dialogActions.children [
Fui.dialogTrigger [
dialogTrigger.action.close
dialogTrigger.children (
Fui.button [
button.icon (Fui.icon.dismissRegular [])
button.text "Cancel"
]
)
]
Fui.button [
button.appearance.primary
button.icon (Fui.icon.deleteRegular [])
button.text "Delete"
button.disabled (not userConfirmed)
button.onClick (fun _ -> handleDeleteArchive ())
]
]
]
]
]
]
]
]
]

View File

@@ -308,7 +308,7 @@ module Admin =
let archmaesterAddPublic (db: Entity.ArchiveContext) (archiveId: System.Guid) =
async {
try
let! success = Archmaester.EFCore.setArchivePublic db archiveId
let! success = Archmaester.EFCore.setArchivePublic db archiveId true
if success > 0 then
return Ok ()
else
@@ -316,20 +316,6 @@ module Admin =
with e ->
return Error (sprintf "Error adding * user to archive: %s" e.Message)
}
let fgaAddPrincipal (fga: OpenFgaClient) (archiveId: System.Guid) =
async {
let tuple = OpenFGA.Archive.defaultPrincipal archiveId
let req = OpenFGA.Queries.write' [|tuple|]
let! _resp = fga.Write req |> Async.AwaitTask
return ()
}
let fgaAddPublic (fga: OpenFgaClient) (archiveId: System.Guid) =
async {
let tuple = OpenFGA.Archive.publicArchive archiveId
let req = OpenFGA.Queries.write' [|tuple|]
let! _resp = fga.Write req |> Async.AwaitTask
return ()
}
asyncResult {
let user = ctx.User.Identity.Name
let logger = ctx.GetLogger<Remoting.Api.Admin> ()
@@ -341,11 +327,27 @@ module Admin =
let! newArchive = archmaesterAdd db
try
do! fgaAddPrincipal fga newArchive.ArchiveId
if newArchive.Public then
do! archmaesterAddPublic db newArchive.ArchiveId
do! fgaAddPublic fga newArchive.ArchiveId
let tuples =
if newArchive.Public then
[|
OpenFGA.Archive.defaultPrincipal newArchive.ArchiveId
OpenFGA.Archive.publicArchive newArchive.ArchiveId
|]
else
[|OpenFGA.Archive.defaultPrincipal newArchive.ArchiveId|]
let! fgaResp =
let req = OpenFGA.Queries.write' tuples
fga.Write req |> Async.AwaitTask
logger.LogInformation (
"addArchive: {Archive} openFGA write responded with {JSON}",
archive.Name,
fgaResp.ToJson ()
)
do! tr.CommitAsync ()
@@ -360,6 +362,87 @@ module Admin =
return! Error "Error adding archive"
}
let updateArchive (ctx: HttpContext) (archiveId: System.Guid) (archive: Remoting.EditArchiveRequest) =
let archmaesterEdit (db: Entity.ArchiveContext) =
async {
try
let! existingName = Archmaester.EFCore.getArchiveName db archiveId
let! nameTaken = Archmaester.EFCore.checkArchiveNameTaken db archive.Name
if nameTaken && not (existingName = archive.Name) then
return Error "Error updating archive: an archive with that name already exists"
else
let! newArchive = Archmaester.EFCore.editArchive db archiveId archive
return Ok newArchive
with e ->
return Error (sprintf "Error updating archive: %s" e.Message)
}
let archmaesterSetPublic (db: Entity.ArchiveContext) (setPublic: bool) =
async {
try
let! success = Archmaester.EFCore.setArchivePublic db archiveId setPublic
if success > 0 then
return Ok ()
else
return Error "Failed to add * user to public archive"
with e ->
return Error (sprintf "Error adding/removing * user to/from archive: %s" e.Message)
}
let fgaSetPublic (fga: OpenFgaClient) (setPublic: bool) =
async {
try
let publicTuple = OpenFGA.Archive.publicArchive archiveId
let checkReq =
OpenFGA.Queries.check { User = publicTuple.User; Relation = publicTuple.Relation; Object = publicTuple.Object}
let! publicResp = fga.Check checkReq |> Async.AwaitTask
let publicTupleExists = publicResp.Allowed |> Option.ofNullable |> Option.defaultValue false
if setPublic && not publicTupleExists then
let writeReq = OpenFGA.Queries.write' [|publicTuple|]
let! writeResp = fga.Write writeReq |> Async.AwaitTask
match writeResp.Writes |> Array.ofSeq with
| [||] -> return Error (sprintf "Error writing public tuple to fga: %s" (writeResp.ToJson()))
| _ -> return Ok ()
elif not setPublic && publicTupleExists then
let deleteReq = OpenFGA.Queries.delete' [|publicTuple|]
let! deleteResp = fga.Write deleteReq |> Async.AwaitTask
match deleteResp.Deletes |> Array.ofSeq with
| [||] -> return Error (sprintf "Error deleting public tuple from fga: %s" (deleteResp.ToJson()))
| _ -> return Ok ()
else
return Ok ()
with e ->
return Error (sprintf "Error adding/removing fga user:* view relation to/from archive: %s" e.Message)
}
asyncResult {
let user = ctx.User.Identity.Name
let logger = ctx.GetLogger<Remoting.Api.Admin> ()
try
do logger.LogInformation("updateArchive {Archive} from {User}", archive.Name, user)
let fga = ctx.GetService<OpenFgaClient> ()
let db = ctx.GetService<Entity.ArchiveContext> ()
let tr = db.Database.BeginTransaction()
let! newArchive = archmaesterEdit db
try
if newArchive.Public && not archive.Public then
do! archmaesterSetPublic db true
do! fgaSetPublic fga true
elif not newArchive.Public && archive.Public then
do! archmaesterSetPublic db false
do! fgaSetPublic fga false
do! tr.CommitAsync ()
return! newArchive |> EFType.toArchive |> Ok
with e ->
do logger.LogError (e, "updateArchive setting public failed, rolling back Archmaester")
do! tr.RollbackAsync ()
return! Error (sprintf "Error updating archive: %s" e.Message)
with e ->
do logger.LogError (e, "updateArchive {Archive} from {User}", archive.Name, user)
return! Error (sprintf "Error updating archive: %s" e.Message)
}
let getArchiveTypes (ctx: HttpContext) =
async {
let user = ctx.User.Identity.Name
@@ -446,6 +529,7 @@ module Admin =
removeUsers = Handler.removeUsers ctx
getDataSets = fun () -> Handler.getAllDataSets ctx
addArchive = Handler.addArchive ctx
updateArchive = Handler.updateArchive ctx
}
let endpoints: HttpHandler =

View File

@@ -250,22 +250,31 @@ module Archmaester =
.AnyAsync()
|> Async.AwaitTask
let getArchiveName (db: Entity.ArchiveContext) (archiveId: System.Guid) : Async<string> =
db.Archives
.AsNoTracking()
.Where(fun archive -> archive.ArchiveId = archiveId)
.Select(_.Name)
.SingleOrDefaultAsync()
|> Async.AwaitTask
/// Align archive start time with the actual files in the dataset
let private getAlignedStartTime (db: Entity.ArchiveContext) (dataSetId: int) (archiveStartTime: System.DateTime) =
let utcStartTime = archiveStartTime.ToUniversalTime()
db.Files
.AsNoTracking()
.Where(fun file ->
file.AttribsId = dataSetId
&& file.StartTime >= utcStartTime
)
.OrderBy(_.StartTime)
.Select(_.StartTime)
.FirstAsync()
|> Async.AwaitTask
let addArchive (db: Entity.ArchiveContext) (archive: Remoting.AddArchiveRequest) : Async<Entity.Archive> =
async {
// NOTE: We need to align the start time with an actual file
let! alignedStartTime =
let dataSetId = archive.DataSetId
let archiveStartTime = archive.StartTime.ToUniversalTime()
db.Files
.AsNoTracking()
.Where(fun file ->
file.AttribsId = dataSetId
&& file.StartTime >= archiveStartTime
)
.OrderBy(_.StartTime)
.Select(_.StartTime)
.FirstAsync()
|> Async.AwaitTask
let! alignedStartTime = getAlignedStartTime db archive.DataSetId archive.StartTime
let newArchive =
Entity.Archive(
@@ -295,9 +304,30 @@ module Archmaester =
return newArchive
}
let setArchivePublic (db: Entity.ArchiveContext) (archiveId: System.Guid) : Async<int> =
let editArchive (db: Entity.ArchiveContext) (archiveId: System.Guid) (archive: Remoting.EditArchiveRequest) : Async<Entity.Archive> =
async {
let! existingArchive =
db.Archives
.Where(fun archive -> archive.ArchiveId = archiveId)
.SingleAsync()
|> Async.AwaitTask
let! alignedStartTime = getAlignedStartTime db existingArchive.AttribsId archive.StartTime
existingArchive.Name <- archive.Name
existingArchive.StartTime <- alignedStartTime
existingArchive.Frames <- archive.Frames
existingArchive.Published <- archive.Published
existingArchive.Public <- archive.Public
do db.Update existingArchive |> ignore
do! db.SaveChangesAsync () |> Async.AwaitTask |> Async.Ignore
return existingArchive
}
let setArchivePublic (db: Entity.ArchiveContext) (archiveId: System.Guid) (setPublic: bool) : Async<int> =
async {
let! starUserId =
db.Users
.AsNoTracking()
@@ -306,12 +336,19 @@ module Archmaester =
.SingleAsync()
|> Async.AwaitTask
let starArchiveUser = Entity.ArchiveUser(UserId = starUserId, ArchiveId = archiveId)
db.Add starArchiveUser |> ignore
if setPublic then
let starArchiveUser = Entity.ArchiveUser(UserId = starUserId, ArchiveId = archiveId)
db.Add starArchiveUser |> ignore
else
let! starArchiveUser =
db.ArchiveUsers
.Where(fun au -> au.UserId = starUserId && au.ArchiveId = archiveId)
.SingleOrDefaultAsync()
|> Async.AwaitTask
let! created = db.SaveChangesAsync () |> Async.AwaitTask
starArchiveUser |> Option.ofObj |> Option.iter (db.Remove >> ignore)
return created
return! db.SaveChangesAsync () |> Async.AwaitTask
}
let deleteArchive (db: Entity.ArchiveContext) (archiveId: System.Guid) : Async<int> =

View File

@@ -148,6 +148,15 @@ module OpenFGA =
result
let read' (tuple: ClientTupleKey) : ClientReadRequest =
let result = ClientReadRequest ()
do result.User <- tuple.User
do result.Relation <- tuple.Relation
do result.Object <- tuple.Object
result
let delete (tuples: Remoting.Tuple array) =
let result = ClientWriteRequest ()
@@ -167,6 +176,25 @@ module OpenFGA =
result
let delete' (tuples: ClientTupleKey array) =
let result = ClientWriteRequest ()
let deletes: ClientTupleKeyWithoutCondition array =
tuples
|> Array.map (fun tuple ->
let result = ClientTupleKeyWithoutCondition ()
do result.Object <- tuple.Object
do result.Relation <- tuple.Relation
do result.User <- tuple.User
result
)
do result.Deletes <- ResizeArray deletes
result
let write (tuples: Remoting.Tuple array) =
let result = ClientWriteRequest ()

View File

@@ -174,6 +174,15 @@ module Remoting =
Public = false
}
[<Struct>]
type EditArchiveRequest = {
Name: string
StartTime: System.DateTime
Frames: int
Published: bool
Public: bool
}
[<Struct>]
type AddUsersRequest = {
Group: string
@@ -206,6 +215,7 @@ module Remoting =
removeUsers: string array -> Async<Result<unit, string>>
getDataSets: unit -> Async<Result<DataSet array, string>>
addArchive: AddArchiveRequest -> Async<Result<Archive, string>>
updateArchive: System.Guid -> EditArchiveRequest -> Async<Result<Archive, string>>
}
type OpenFGA = {