Compare commits

...

5 Commits

Author SHA1 Message Date
c66cda70be codex: Style tweaks to sub archive section 2025-12-03 17:17:42 +01:00
bff84f8ff2 Add Props title to archive info section 2025-12-03 17:08:22 +01:00
8d09b544a5 Remove unused/not needed fetch 2025-12-03 17:07:50 +01:00
Simen Kirkvik
af527a6754 Export APP_ENV in .envrc 2025-11-29 16:20:33 +01:00
Simen Kirkvik
6b64d9e197 tmp(codex): Fetch archive associations 2025-11-29 16:20:12 +01:00
5 changed files with 245 additions and 160 deletions

3
.envrc
View File

@@ -1,4 +1,7 @@
#!/usr/bin/env bash
export APP_ENV=$USER
# the shebang is ignored, but nice for editors
watch_file nix/sources.json

View File

@@ -13,18 +13,14 @@ module Archive =
let! acl = Remoting.aclApi.getAcl id |> Async.StartAsPromise
return acl
with e ->
console.error("Error fetching archive ACL: %o", e)
console.error ("Error fetching archive ACL: %o", e)
return Error "Error fetching archive ACL"
}
let private fetchSubArchives (id: System.Guid) : JS.Promise<Result<Archmaester.Dto.ArchiveProps array, string>> =
promise {
let archiveType = Archmaester.Dto.ArchiveType.FromString "*:*:*"
let filter = {
Archmaester.Dto.ArchiveFilter.empty with
id = Some id
archiveType = Some archiveType
}
let filter = { Archmaester.Dto.ArchiveFilter.empty with id = Some id; archiveType = Some archiveType }
let! subs = Remoting.adminApi.getArchiveRefs filter |> Async.StartAsPromise
return subs
}
@@ -35,7 +31,7 @@ module Archive =
let! res = Remoting.adminApi.deleteArchive id |> Async.StartAsPromise
return res
with e ->
console.error("Error deleting archive: %o", e)
console.error ("Error deleting archive: %o", e)
return Error "Error deleting archive"
}
@@ -50,7 +46,7 @@ module Archive =
[<ReactComponent>]
let private GroupSelect (onChange: string option -> unit) =
let groupsReq = Groups.useGroups()
let groupsReq = Groups.useGroups ()
let options =
let groupOptions =
@@ -73,7 +69,7 @@ module Archive =
Html.select [
prop.onChange (fun (ev: Types.Event) ->
let group : string = ev.target?value
let group: string = ev.target?value
if group = "none" then
onChange None
else
@@ -85,14 +81,13 @@ module Archive =
[<ReactComponent>]
let private GroupFGAList (object: string) (id: string) relation userFilter ctx =
let groups = OpenFGA.useUsers(object, id, relation, userFilter, ctx)
let groups = OpenFGA.useUsers (object, id, relation, userFilter, ctx)
if Array.isEmpty groups.Objects then
Html.p "No objects"
else
match groups.Error with
| Some err ->
Html.p err
| Some err -> Html.p err
| None ->
Html.ul [
prop.children (
@@ -102,7 +97,7 @@ module Archive =
prop.key object
prop.children [
Html.a [
prop.href (Router.format("groups", object))
prop.href (Router.format ("groups", object))
prop.text object
]
]
@@ -113,14 +108,13 @@ module Archive =
[<ReactComponent>]
let private UserFGAList (object: string) (id: string) relation userFilter ctx =
let groups = OpenFGA.useUsers(object, id, relation, userFilter, ctx)
let groups = OpenFGA.useUsers (object, id, relation, userFilter, ctx)
if Array.isEmpty groups.Objects then
Html.p "No objects"
else
match groups.Error with
| Some msg ->
Html.p msg
| Some msg -> Html.p msg
| None ->
Html.ul [
prop.children (
@@ -130,7 +124,7 @@ module Archive =
prop.key object
prop.children [
Html.a [
prop.href (Router.format("user", object))
prop.href (Router.format ("user", object))
prop.text object
]
]
@@ -140,7 +134,7 @@ module Archive =
]
[<ReactComponent>]
let SubArchives (archiveId: System.Guid) =
let private SubArchives (archiveId: System.Guid) =
let loading, setLoading = React.useState true
let archives, setArchives = React.useState<Archmaester.Dto.ArchiveProps array> [||]
@@ -166,8 +160,7 @@ module Archive =
|> Promise.iter (fun res ->
match res with
| Ok archives -> setArchives archives
| Error err ->
console.error("Error fetching archive %s", err)
| Error err -> console.error ("Error fetching archive %s", err)
setLoading false
)
),
@@ -178,47 +171,62 @@ module Archive =
Html.div [
prop.classes [ "grow" ]
prop.style [
style.flexBasis (length.px 512)
style.minWidth (length.px 512)
style.maxWidth (length.px 768)
style.flexBasis (length.px 384)
style.minWidth (length.px 384)
style.maxWidth (length.px 512)
]
prop.children [
Html.h2 "Sub archives"
if loading then
Html.p "Loading ..."
else
if Array.isEmpty archives then
Html.p "No sub archives"
else
Html.ul [
prop.children [
Html.li [
prop.children [
Html.text "Archives"
Html.div [
prop.style [
style.maxHeight (length.px 512)
style.overflowY.auto
]
prop.children [
if loading then
Html.p "Loading ..."
else if Array.isEmpty archives then
Html.p "No sub archives"
else
Html.ul [
prop.children [
Html.li [
prop.children [
Html.text "Archives"
Html.ul [
prop.children (
rest
|> Array.map (fun archive ->
Html.li [
prop.key archive.archiveId
prop.children [
Html.a [
prop.href (Router.format ("archives", string archive.archiveId))
prop.text archive.name
Html.ul [
prop.children (
rest
|> Array.map (fun archive ->
Html.li [
prop.key archive.archiveId
prop.children [
Html.a [
prop.href (
Router.format (
"archives",
string archive.archiveId
)
)
prop.text archive.name
]
]
]
]
)
)
)
]
]
]
]
Drifters.List (drifters |> Array.map (fun prop -> { Props = prop; CanView = false; CanExec = false }))
Drifters.List (
drifters
|> Array.map (fun prop -> { Props = prop; CanView = false; CanExec = false })
)
]
]
]
]
]
]
]
]
@@ -231,7 +239,8 @@ module Archive =
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 archiveOpt, setArchive =
React.useState<Archmaester.Dto.ArchiveProps option> None
let aclOpt, setAcl = React.useState<Archmaester.Dto.ArchiveAcl option> None
let handleAddGroupToArchiveAcl (archiveId: System.Guid) =
@@ -239,23 +248,17 @@ module Archive =
| Some acl ->
match selectedGroup with
| Some group ->
console.debug("Adding group %s to archive %s", selectedGroup, archiveId)
console.debug ("Adding group %s to archive %s", selectedGroup, archiveId)
addArchiveGroup archiveId group
|> Promise.iter (fun res ->
match res with
| Ok () ->
let newAcl = {
acl with
groups = [| group |] |> Array.append acl.groups
}
let newAcl = { acl with groups = [| group |] |> Array.append acl.groups }
setAcl (Some newAcl)
| Error err ->
setError (Some err)
| Error err -> setError (Some err)
)
| None ->
console.warn("No group selected to add to archive %s", archiveId)
| None ->
console.warn("ACL has not been downloaded")
| None -> console.warn ("No group selected to add to archive %s", archiveId)
| None -> console.warn ("ACL has not been downloaded")
let handleDeleteArchive (id: System.Guid) =
deleteArchive id
@@ -263,17 +266,17 @@ module Archive =
match res with
| Ok deleted ->
if deleted then
console.info("Archive deleted successfully")
console.info ("Archive deleted successfully")
setDeleted true
else
setError (Some "Failed to delete archive")
| Error err ->
console.error("Error deleting archive: %s", err)
console.error ("Error deleting archive: %s", err)
setError (Some err)
)
let handleSelectedGroupChange (groupOpt: string option) =
console.debug("Selected group: %s", groupOpt)
console.debug ("Selected group: %s", groupOpt)
setSelectedGroup groupOpt
React.useEffect (
@@ -290,13 +293,12 @@ module Archive =
| Ok acl ->
setArchive (Some archive)
setAcl (Some acl)
| Error err ->
setError (Some err)
| Error err -> setError (Some err)
setLoading false
)
| Error err ->
console.error("Error fetching archive: %s", err)
console.error ("Error fetching archive: %s", err)
setLoading false
setError (Some err)
)
@@ -338,7 +340,10 @@ module Archive =
]
else
Html.div [
prop.classes [ "flex-row"; "gap-8" ]
prop.classes [
"flex-row"
"gap-8"
]
prop.children [
if deleting then
Html.button [
@@ -350,37 +355,27 @@ module Archive =
]
Html.button [
prop.onClick (fun ev ->
setDeleting false
)
prop.onClick (fun ev -> setDeleting false)
prop.text "Cancel"
]
elif editing then
Html.button [
prop.onClick (fun ev ->
setEditing false
)
prop.onClick (fun ev -> setEditing false)
prop.text "Save"
]
Html.button [
prop.onClick (fun ev ->
setEditing false
)
prop.onClick (fun ev -> setEditing false)
prop.text "Cancel"
]
else
Html.button [
prop.onClick (fun ev ->
setEditing true
)
prop.onClick (fun ev -> setEditing true)
prop.text "Edit"
]
Html.button [
prop.onClick (fun ev ->
setDeleting true
)
prop.onClick (fun ev -> setDeleting true)
prop.text "Delete"
]
]
@@ -411,8 +406,14 @@ module Archive =
Archives.InfoSection archive
Html.div [
prop.classes [ "flex-row"; "flex-wrap"; "gap-32" ]
prop.classes [
"flex-row"
"flex-wrap"
"gap-32"
]
prop.children [
SubArchives archiveId
match aclOpt with
| Some acl ->
Html.div [
@@ -426,7 +427,10 @@ module Archive =
Html.h2 "Groups"
Html.div [
prop.classes [ "flex-row"; "gap-8" ]
prop.classes [
"flex-row"
"gap-8"
]
prop.children [
Html.button [
prop.disabled selectedGroup.IsNone
@@ -441,7 +445,11 @@ module Archive =
]
Html.div [
prop.classes [ "flex-row"; "flex-wrap"; "gap-32" ]
prop.classes [
"flex-row"
"flex-wrap"
"gap-32"
]
prop.children [
Html.div [
prop.classes [ "grow" ]
@@ -465,7 +473,12 @@ module Archive =
prop.key group
prop.children [
Html.a [
prop.href (Router.format("groups", group))
prop.href (
Router.format (
"groups",
group
)
)
prop.text group
]
]
@@ -487,7 +500,11 @@ module Archive =
Html.h3 "OpenFGA"
Html.div [
prop.classes [ "flex-row"; "flex-wrap"; "gap-32" ]
prop.classes [
"flex-row"
"flex-wrap"
"gap-32"
]
prop.children [
Html.div [
prop.classes [ "grow" ]
@@ -497,7 +514,10 @@ module Archive =
"archive"
(string archive.archiveId)
"view"
{ Type = "group"; Relation = Some "member" }
{
Type = "group"
Relation = Some "member"
}
{| time = System.DateTime.Now |}
]
]
@@ -506,12 +526,16 @@ module Archive =
prop.classes [ "grow" ]
prop.children [
Html.h4 "Groups who's members can exec"
Html.p "With task '*', usage '-1' and current time"
Html.p
"With task '*', usage '-1' and current time"
GroupFGAList
"archive"
(string archive.archiveId)
"exec"
{ Type = "group"; Relation = Some "member" }
{
Type = "group"
Relation = Some "member"
}
{|
task = "*"
usage = "-1"
@@ -539,7 +563,11 @@ module Archive =
Html.h2 "Owners"
Html.div [
prop.classes [ "flex-row"; "flex-wrap"; "gap-8" ]
prop.classes [
"flex-row"
"flex-wrap"
"gap-8"
]
prop.children [
Html.div [
prop.classes [ "grow" ]
@@ -558,7 +586,12 @@ module Archive =
prop.key owner
prop.children [
Html.a [
prop.href (Router.format("users", owner))
prop.href (
Router.format (
"users",
owner
)
)
prop.text owner
]
]
@@ -608,7 +641,9 @@ module Archive =
prop.key user
prop.children [
Html.a [
prop.href (Router.format("users", user))
prop.href (
Router.format ("users", user)
)
prop.text user
]
]
@@ -624,7 +659,11 @@ module Archive =
Html.h3 "OpenFGA"
Html.div [
prop.classes [ "flex-row"; "flex-wrap"; "gap-32" ]
prop.classes [
"flex-row"
"flex-wrap"
"gap-32"
]
prop.children [
Html.div [
prop.classes [ "grow" ]
@@ -703,21 +742,14 @@ module Archive =
|> Array.map (fun share ->
Html.li [
prop.key share
prop.children [
Html.a [
prop.text (string share)
]
]
prop.children [ Html.a [ prop.text (string share) ] ]
]
)
)
]
]
]
| None ->
Html.h2 "No ACL found"
SubArchives archiveId
| None -> Html.h2 "No ACL found"
match Utils.tryStr archive.json with
| Some jsonStr ->
@@ -731,17 +763,13 @@ module Archive =
style.maxHeight (length.px 512)
style.overflowY.scroll
]
prop.children [
Html.pre (JS.JSON.stringify(json, space = 4))
]
prop.children [ Html.pre (JS.JSON.stringify (json, space = 4)) ]
]
]
]
| None ->
Html.none
| None -> Html.none
]
]
| None ->
Html.h1 "Archive not found"
]
| None -> Html.h1 "Archive not found"
]

View File

@@ -1,8 +1,34 @@
namespace Oceanbox.Codex
open Feliz
open Feliz.Router
type Archives =
[<ReactComponent>]
static member RefLink (archiveId: System.Guid) =
let archiveOpt, setArchive = React.useState<Archmaester.Dto.ArchiveProps option> None
React.useEffect (
(fun () ->
Archives.Utils.fetchArchive archiveId
|> Promise.iter (fun res ->
match res with
| Ok archive ->
setArchive (Some archive)
| Error err ->
Browser.Dom.console.error ("Error fetching archive: %s", err)
)
),
[| box archiveId |]
)
Html.a [
prop.href (Router.format("archives", string archiveId))
match archiveOpt with
| Some archive -> prop.text archive.name
| None -> prop.text "link"
]
[<ReactComponent>]
static member InfoSection(archive: Archmaester.Dto.ArchiveProps) =
let archiveLength : System.TimeSpan = archive.endTime - archive.startTime
@@ -13,49 +39,67 @@ type Archives =
Html.section [
prop.classes [ "flex-row"; "flex-wrap"; "gap-16" ]
prop.children [
Html.ul [
Html.div [
prop.style [
style.flexBasis (length.px 384)
]
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")
Html.h2 "Props"
Html.ul [
prop.children [
Html.li [
prop.text (sprintf "Description: %s" archive.description)
]
Html.li [
prop.text (sprintf "Archive type: %s" (string archive.archiveType))
]
match archive.reference with
| Some refId ->
Html.li [
prop.children [
Html.text "Parent: "
Archives.RefLink refId
]
]
| None -> ()
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")
]
]
]
]
]
@@ -69,4 +113,4 @@ type Archives =
prop.children (Map.View focalPoint [||])
]
]
]
]

View File

@@ -179,8 +179,8 @@ module Admin =
else
return Error "Filter must include archive id"
with e ->
do logger.LogError (e, "Error in getArchives from {User}", user)
return Error "Error fetching archive count"
do logger.LogError (e, "Error in getArchiveRefs from {User}", user)
return Error "Error fetching archive refs"
}
let getArchives
@@ -325,4 +325,4 @@ module Admin =
|> Remoting.withErrorHandler Utils.rpcErrorHandler
|> Remoting.fromContext impl
|> Remoting.withRouteBuilder Remoting.routeBuilder
|> Remoting.buildHttpHandler
|> Remoting.buildHttpHandler

View File

@@ -280,12 +280,22 @@ module Archmaester =
async {
let archiveId = filter.id |> Option.toNullable
let! wantedArchive =
db.Archives
.AsNoTracking()
.SingleAsync(fun archive -> if filter.id.IsSome then archive.ArchiveId = filter.id.Value else false)
|> Async.AwaitTask
let! entities =
db.Archives
.AsNoTracking()
.Where(fun archive -> archive.RefId ?=? archiveId)
.Include(fun archive -> archive.Attribs)
.ThenInclude(fun attribs -> attribs.Type)
.Where(fun archive ->
archive.RefId ?=? archiveId
|| archive.Attribs.Associations.Any(fun ass ->
ass.RefId = wantedArchive.AttribsId
)
)
.ToArrayAsync ()
|> Async.AwaitTask
@@ -420,4 +430,4 @@ module Archmaester =
|> Async.AwaitTask
return entities
}
}