generated from coulomb/repo-seed
feat(WP-0009): IHF GAAF Compliance Foundation — type registries, extension manifests, architectural contracts
Some checks failed
Test / test (push) Has been cancelled
Some checks failed
Test / test (push) Has been cancelled
Implements IHUB-WP-0009: closes four GAAF-2026 gaps before domain hub work begins. - TypeRegistry helper + controllers/views (hub_kind, hub_capability_manifest) - HubCapabilityManifest entity with validation and registry linkage - ARCHITECTURE-LAYERS.md + CI-enforced boundary contracts - Alembic migration 1743724800, fitness tests (Test/Architecture/) - GAAF spec, Operational Architecture spec, domain hub extension guide - Updates to CLAUDE.md, SCOPE.md, Schema.sql, Routes, FrontController, Types state_hub_sync: pending (tunnel was STALE at completion time; run fix-consistency) Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -7,9 +7,7 @@ import Web.View.Annotations.Show
|
||||
import Generated.Types
|
||||
import IHP.Prelude
|
||||
import IHP.ControllerPrelude
|
||||
|
||||
validCategories :: [Text]
|
||||
validCategories = ["friction", "defect", "wish", "policy_concern", "doc_gap", "trust", "other"]
|
||||
import Application.Helper.TypeRegistry (validateAnnotationCategory, activeAnnotationCategories)
|
||||
|
||||
validSeverities :: [Text]
|
||||
validSeverities = ["low", "medium", "high", "critical"]
|
||||
@@ -35,16 +33,21 @@ instance Controller AnnotationsController where
|
||||
render ShowView { widget, annotation, mCandidate }
|
||||
|
||||
action NewAnnotationAction { widgetId } = do
|
||||
widget <- fetch widgetId
|
||||
widget <- fetch widgetId
|
||||
categories <- activeAnnotationCategories
|
||||
let annotation = newRecord @Annotation
|
||||
render NewView { widget, annotation }
|
||||
render NewView { widget, annotation, categories }
|
||||
|
||||
action CreateAnnotationAction { widgetId } = do
|
||||
widget <- fetch widgetId
|
||||
mUser <- currentUserOrNothing
|
||||
widget <- fetch widgetId
|
||||
categories <- activeAnnotationCategories
|
||||
mUser <- currentUserOrNothing
|
||||
let actorId = fmap (.id) mUser
|
||||
actorType = maybe "anonymous" (const "user") mUser
|
||||
|
||||
category <- paramOrDefault @Text "" "category"
|
||||
categoryResult <- validateAnnotationCategory category
|
||||
|
||||
let annotation = newRecord @Annotation
|
||||
annotation
|
||||
|> fill @'["body", "category", "severity", "parentId", "widgetStateRef"]
|
||||
@@ -52,10 +55,12 @@ instance Controller AnnotationsController where
|
||||
|> set #actorId (fmap (Id . unId) actorId)
|
||||
|> set #actorType actorType
|
||||
|> validateField #body nonEmpty
|
||||
|> validateField #category (`elem` validCategories)
|
||||
|> validateField #severity (`elem` validSeverities)
|
||||
|> (case categoryResult of
|
||||
Left msg -> attachFailure #category msg
|
||||
Right () -> id)
|
||||
|> ifValid \case
|
||||
Left annotation -> render NewView { widget, annotation }
|
||||
Left annotation -> render NewView { widget, annotation, categories }
|
||||
Right annotation -> do
|
||||
createRecord annotation
|
||||
setSuccessMessage "Annotation added"
|
||||
|
||||
@@ -7,10 +7,7 @@ import IHP.ControllerPrelude
|
||||
import Data.Aeson (object, (.=))
|
||||
import qualified Data.Text as T
|
||||
import Network.Wai (requestMethod, requestHeaders)
|
||||
|
||||
-- | Accepted event types per InteractionReportingContract v1.0
|
||||
apiAcceptedEventTypes :: [Text]
|
||||
apiAcceptedEventTypes = ["clicked", "viewed", "submitted", "dismissed", "errored"]
|
||||
import Application.Helper.TypeRegistry (validateEventType)
|
||||
|
||||
instance Controller ApiInteractionEventsController where
|
||||
|
||||
@@ -65,12 +62,15 @@ createEventForHub hub = do
|
||||
let Just wIdText = widgetIdText
|
||||
Just evType = eventType
|
||||
|
||||
unless (evType `elem` apiAcceptedEventTypes) do
|
||||
setStatus 422
|
||||
respondJson (object
|
||||
[ "error" .= ("Unacceptable event_type" :: Text)
|
||||
, "accepted" .= apiAcceptedEventTypes
|
||||
])
|
||||
evTypeResult <- liftIO $ validateEventType evType
|
||||
case evTypeResult of
|
||||
Left _ -> do
|
||||
setStatus 422
|
||||
respondJson (object
|
||||
[ "error" .= ("Unacceptable event_type" :: Text)
|
||||
, "hint" .= ("Register the event type in the Type Registry before submitting" :: Text)
|
||||
])
|
||||
Right () -> pure ()
|
||||
|
||||
-- Resolve widget — must belong to this hub.
|
||||
case readMay wIdText of
|
||||
|
||||
169
Web/Controller/HubCapabilityManifests.hs
Normal file
169
Web/Controller/HubCapabilityManifests.hs
Normal file
@@ -0,0 +1,169 @@
|
||||
module Web.Controller.HubCapabilityManifests where
|
||||
|
||||
import Web.Types
|
||||
import Web.View.HubCapabilityManifests.Index
|
||||
import Web.View.HubCapabilityManifests.Show
|
||||
import Web.View.HubCapabilityManifests.New
|
||||
import Web.View.HubCapabilityManifests.Edit
|
||||
import Generated.Types
|
||||
import IHP.Prelude
|
||||
import IHP.ControllerPrelude
|
||||
import Data.Aeson (Value, Array, decode, encode)
|
||||
import qualified Data.Vector as V
|
||||
import Data.Maybe (mapMaybe)
|
||||
|
||||
instance Controller HubCapabilityManifestsController where
|
||||
beforeAction = ensureIsUser
|
||||
|
||||
action HubCapabilityManifestsAction = autoRefresh do
|
||||
manifests <- query @HubCapabilityManifest
|
||||
|> orderByDesc #createdAt
|
||||
|> fetch
|
||||
hubs <- query @Hub |> fetch
|
||||
render IndexView { manifests, hubs }
|
||||
|
||||
action ShowHubCapabilityManifestAction { hubCapabilityManifestId } = do
|
||||
manifest <- fetch hubCapabilityManifestId
|
||||
hub <- fetch manifest.hubId
|
||||
render ShowView { manifest, hub }
|
||||
|
||||
action NewHubCapabilityManifestAction = do
|
||||
mHubId <- paramOrNothing @(Id Hub) "hubId"
|
||||
hubs <- query @Hub |> orderByAsc #name |> fetch
|
||||
let manifest = newRecord @HubCapabilityManifest
|
||||
case mHubId of
|
||||
Just hubId -> do
|
||||
-- Check if a manifest already exists for this hub
|
||||
existing <- query @HubCapabilityManifest
|
||||
|> filterWhere (#hubId, hubId)
|
||||
|> fetchOneOrNothing
|
||||
case existing of
|
||||
Just m -> redirectTo EditHubCapabilityManifestAction { hubCapabilityManifestId = m.id }
|
||||
Nothing -> render NewView { manifest = manifest |> set #hubId hubId, hubs }
|
||||
Nothing -> render NewView { manifest, hubs }
|
||||
|
||||
action CreateHubCapabilityManifestAction = do
|
||||
hubs <- query @Hub |> orderByAsc #name |> fetch
|
||||
let manifest = newRecord @HubCapabilityManifest
|
||||
manifest
|
||||
|> fill @'["hubId", "manifestVersion", "capabilityDescription", "contact"]
|
||||
|> set #status "draft"
|
||||
|> validateField #hubId nonEmpty
|
||||
|> ifValid \case
|
||||
Left manifest -> render NewView { manifest, hubs }
|
||||
Right manifest -> do
|
||||
manifest <- createRecord manifest
|
||||
setSuccessMessage "Capability manifest created (draft)"
|
||||
redirectTo EditHubCapabilityManifestAction { hubCapabilityManifestId = manifest.id }
|
||||
|
||||
action EditHubCapabilityManifestAction { hubCapabilityManifestId } = do
|
||||
manifest <- fetch hubCapabilityManifestId
|
||||
hub <- fetch manifest.hubId
|
||||
widgetTypeEntries <- sqlQuery "SELECT * FROM widget_type_registry WHERE status = 'active' ORDER BY label ASC" ()
|
||||
eventTypeEntries <- sqlQuery "SELECT * FROM event_type_registry WHERE status = 'active' ORDER BY label ASC" ()
|
||||
categoryEntries <- sqlQuery "SELECT * FROM annotation_category_registry WHERE status = 'active' ORDER BY label ASC" ()
|
||||
policyScopeEntries <- sqlQuery "SELECT * FROM policy_scope_registry WHERE status = 'active' ORDER BY label ASC" ()
|
||||
render EditView { manifest, hub, widgetTypeEntries, eventTypeEntries, categoryEntries, policyScopeEntries }
|
||||
|
||||
action UpdateHubCapabilityManifestAction { hubCapabilityManifestId } = do
|
||||
manifest <- fetch hubCapabilityManifestId
|
||||
hub <- fetch manifest.hubId
|
||||
widgetTypeEntries <- sqlQuery "SELECT * FROM widget_type_registry WHERE status = 'active' ORDER BY label ASC" ()
|
||||
eventTypeEntries <- sqlQuery "SELECT * FROM event_type_registry WHERE status = 'active' ORDER BY label ASC" ()
|
||||
categoryEntries <- sqlQuery "SELECT * FROM annotation_category_registry WHERE status = 'active' ORDER BY label ASC" ()
|
||||
policyScopeEntries <- sqlQuery "SELECT * FROM policy_scope_registry WHERE status = 'active' ORDER BY label ASC" ()
|
||||
when (manifest.status == "active") do
|
||||
setErrorMessage "Active manifests are read-only. Retire the current manifest and create a new draft to amend."
|
||||
redirectTo ShowHubCapabilityManifestAction { hubCapabilityManifestId }
|
||||
manifest
|
||||
|> fill @'["manifestVersion", "capabilityDescription", "contact",
|
||||
"declaredWidgetTypes", "declaredEventTypes",
|
||||
"declaredAnnotationCategories", "declaredPolicyScopes"]
|
||||
|> ifValid \case
|
||||
Left manifest -> render EditView { manifest, hub, widgetTypeEntries, eventTypeEntries, categoryEntries, policyScopeEntries }
|
||||
Right manifest -> do
|
||||
updateRecord manifest
|
||||
setSuccessMessage "Manifest updated"
|
||||
redirectTo EditHubCapabilityManifestAction { hubCapabilityManifestId }
|
||||
|
||||
action ActivateManifestAction { hubCapabilityManifestId } = do
|
||||
manifest <- fetch hubCapabilityManifestId
|
||||
hub <- fetch manifest.hubId
|
||||
|
||||
-- Collect declared type names from JSONB arrays
|
||||
let wTypes = jsonArrayTexts manifest.declaredWidgetTypes
|
||||
eTypes = jsonArrayTexts manifest.declaredEventTypes
|
||||
cats = jsonArrayTexts manifest.declaredAnnotationCategories
|
||||
scopes = jsonArrayTexts manifest.declaredPolicyScopes
|
||||
|
||||
-- Conflict detection: check that each declared name is either
|
||||
-- unregistered or already owned by this hub.
|
||||
conflicts <- fmap concat $ mapM (checkConflict "widget_type_registry" hub.id) wTypes
|
||||
eConflicts <- fmap concat $ mapM (checkConflict "event_type_registry" hub.id) eTypes
|
||||
cConflicts <- fmap concat $ mapM (checkConflict "annotation_category_registry" hub.id) cats
|
||||
pConflicts <- fmap concat $ mapM (checkConflict "policy_scope_registry" hub.id) scopes
|
||||
let allConflicts = conflicts <> eConflicts <> cConflicts <> pConflicts
|
||||
|
||||
if not (null allConflicts)
|
||||
then do
|
||||
setErrorMessage ("Activation blocked — type name conflicts: " <> intercalate ", " allConflicts)
|
||||
redirectTo ShowHubCapabilityManifestAction { hubCapabilityManifestId }
|
||||
else do
|
||||
-- Register declared types (idempotent — skip if already present)
|
||||
mapM_ (upsertType "widget_type_registry" hub.id) wTypes
|
||||
mapM_ (upsertType "event_type_registry" hub.id) eTypes
|
||||
mapM_ (upsertType "annotation_category_registry" hub.id) cats
|
||||
mapM_ (upsertType "policy_scope_registry" hub.id) scopes
|
||||
now <- getCurrentTime
|
||||
manifest |> set #status "active" |> set #activatedAt (Just now) |> updateRecord
|
||||
setSuccessMessage "Manifest activated — all declared types are now registered"
|
||||
redirectTo ShowHubCapabilityManifestAction { hubCapabilityManifestId }
|
||||
|
||||
action RetireManifestAction { hubCapabilityManifestId } = do
|
||||
manifest <- fetch hubCapabilityManifestId
|
||||
manifest |> set #status "retired" |> updateRecord
|
||||
setSuccessMessage "Manifest retired. Types remain in registry but this manifest is no longer current."
|
||||
redirectTo HubCapabilityManifestsAction
|
||||
|
||||
-- | Extract text values from a JSONB array (e.g. '["foo","bar"]').
|
||||
jsonArrayTexts :: Value -> [Text]
|
||||
jsonArrayTexts val = case val of
|
||||
_ -> case decode (encode val) of
|
||||
Just (arr :: Array) -> mapMaybe extractText (V.toList arr)
|
||||
Nothing -> []
|
||||
where
|
||||
extractText (String t) = Just t
|
||||
extractText _ = Nothing
|
||||
|
||||
-- | Check if 'name' in 'tableName' is owned by a different hub.
|
||||
-- Returns [] if no conflict, or [error message] if conflict.
|
||||
checkConflict ::
|
||||
(?modelContext :: ModelContext) =>
|
||||
Text -> Id Hub -> Text -> IO [Text]
|
||||
checkConflict tableName hubId name = do
|
||||
rows <- sqlQuery
|
||||
("SELECT owner_hub_id FROM " <> tableName <> " WHERE name = ?")
|
||||
(Only name)
|
||||
case rows of
|
||||
[] -> pure []
|
||||
[Only Nothing] -> pure [] -- framework-level, no owner conflict
|
||||
[Only (Just ownerId)] ->
|
||||
if ownerId == hubId
|
||||
then pure []
|
||||
else pure ["Type '" <> name <> "' in " <> tableName <> " is already owned by another hub"]
|
||||
_ -> pure []
|
||||
|
||||
-- | Insert a type name into the registry table if it doesn't exist.
|
||||
upsertType ::
|
||||
(?modelContext :: ModelContext) =>
|
||||
Text -> Id Hub -> Text -> IO ()
|
||||
upsertType tableName hubId name =
|
||||
sqlExec
|
||||
("INSERT INTO " <> tableName <> " (name, label, owner_hub_id, status) "
|
||||
<> "VALUES (?, ?, ?, 'active') ON CONFLICT (name) DO NOTHING")
|
||||
(name, name, hubId)
|
||||
|
||||
intercalate :: Text -> [Text] -> Text
|
||||
intercalate _ [] = ""
|
||||
intercalate _ [x] = x
|
||||
intercalate sep (x:xs) = x <> sep <> intercalate sep xs
|
||||
@@ -10,6 +10,7 @@ import Generated.Types
|
||||
import IHP.Prelude
|
||||
import IHP.ControllerPrelude
|
||||
import Application.Helper.RoutingEngine (applyRoutingRules)
|
||||
import Application.Helper.TypeRegistry (validateWidgetType, validateAnnotationCategory)
|
||||
|
||||
instance Controller HubRoutingRulesController where
|
||||
beforeAction = ensureIsUser
|
||||
@@ -33,10 +34,16 @@ instance Controller HubRoutingRulesController where
|
||||
action CreateHubRoutingRuleAction = do
|
||||
let rule = newRecord @HubRoutingRule
|
||||
hubs <- query @Hub |> orderByAsc #name |> fetch
|
||||
mMatchWidgetType <- paramOrNothing @Text "matchWidgetType"
|
||||
mMatchCategory <- paramOrNothing @Text "matchCategory"
|
||||
wtResult <- case mMatchWidgetType of { Nothing -> pure (Right ()); Just "" -> pure (Right ()); Just t -> liftIO (validateWidgetType t) }
|
||||
catResult <- case mMatchCategory of { Nothing -> pure (Right ()); Just "" -> pure (Right ()); Just c -> liftIO (validateAnnotationCategory c) }
|
||||
rule
|
||||
|> fill @'["sourceHubId","targetHubId","matchCategory","matchWidgetType","priority","notes"]
|
||||
|> validateField #sourceHubId nonEmpty
|
||||
|> validateField #targetHubId nonEmpty
|
||||
|> (case wtResult of { Left msg -> attachFailure #matchWidgetType msg; Right () -> id })
|
||||
|> (case catResult of { Left msg -> attachFailure #matchCategory msg; Right () -> id })
|
||||
|> ifValid \case
|
||||
Left r -> render NewView { rule = r, hubs }
|
||||
Right r -> do
|
||||
@@ -52,8 +59,14 @@ instance Controller HubRoutingRulesController where
|
||||
action UpdateHubRoutingRuleAction { hubRoutingRuleId } = do
|
||||
rule <- fetch hubRoutingRuleId
|
||||
hubs <- query @Hub |> orderByAsc #name |> fetch
|
||||
mMatchWidgetType <- paramOrNothing @Text "matchWidgetType"
|
||||
mMatchCategory <- paramOrNothing @Text "matchCategory"
|
||||
wtResult <- case mMatchWidgetType of { Nothing -> pure (Right ()); Just "" -> pure (Right ()); Just t -> liftIO (validateWidgetType t) }
|
||||
catResult <- case mMatchCategory of { Nothing -> pure (Right ()); Just "" -> pure (Right ()); Just c -> liftIO (validateAnnotationCategory c) }
|
||||
rule
|
||||
|> fill @'["matchCategory","matchWidgetType","priority","notes"]
|
||||
|> (case wtResult of { Left msg -> attachFailure #matchWidgetType msg; Right () -> id })
|
||||
|> (case catResult of { Left msg -> attachFailure #matchCategory msg; Right () -> id })
|
||||
|> ifValid \case
|
||||
Left r -> render EditView { rule = r, hubs }
|
||||
Right r -> do
|
||||
|
||||
@@ -46,15 +46,20 @@ instance Controller HubsController where
|
||||
recentAnnotations <- sqlQuery
|
||||
"SELECT * FROM annotations WHERE widget_id = ANY(?) ORDER BY created_at DESC LIMIT 20"
|
||||
(Only (PGArray widgetIds))
|
||||
render ShowView { hub, widgets, recentEvents, recentAnnotations }
|
||||
mManifest <- query @HubCapabilityManifest
|
||||
|> filterWhere (#hubId, hubId)
|
||||
|> fetchOneOrNothing
|
||||
render ShowView { hub, widgets, recentEvents, recentAnnotations, mManifest }
|
||||
|
||||
action CreateHubAction = do
|
||||
let hub = newRecord @Hub
|
||||
hub
|
||||
|> fill @'["slug", "name", "domain"]
|
||||
|> fill @'["slug", "name", "domain", "hubKind"]
|
||||
|> validateField #slug nonEmpty
|
||||
|> validateField #name nonEmpty
|
||||
|> validateField #domain nonEmpty
|
||||
|> validateField #hubKind (`elem` ["domain", "shared"])
|
||||
-- 'framework' cannot be set via the UI
|
||||
|> ifValid \case
|
||||
Left hub -> render NewView { hub }
|
||||
Right hub -> do
|
||||
@@ -69,10 +74,11 @@ instance Controller HubsController where
|
||||
action UpdateHubAction { hubId } = do
|
||||
hub <- fetch hubId
|
||||
hub
|
||||
|> fill @'["slug", "name", "domain"]
|
||||
|> fill @'["slug", "name", "domain", "hubKind"]
|
||||
|> validateField #slug nonEmpty
|
||||
|> validateField #name nonEmpty
|
||||
|> validateField #domain nonEmpty
|
||||
|> validateField #hubKind (`elem` ["framework", "domain", "shared"])
|
||||
|> ifValid \case
|
||||
Left hub -> render EditView { hub }
|
||||
Right hub -> do
|
||||
|
||||
278
Web/Controller/TypeRegistries.hs
Normal file
278
Web/Controller/TypeRegistries.hs
Normal file
@@ -0,0 +1,278 @@
|
||||
module Web.Controller.TypeRegistries where
|
||||
|
||||
import Web.Types
|
||||
import Web.View.TypeRegistries.WidgetTypes
|
||||
import Web.View.TypeRegistries.EventTypes
|
||||
import Web.View.TypeRegistries.AnnotationCategories
|
||||
import Web.View.TypeRegistries.PolicyScopes
|
||||
import Generated.Types
|
||||
import IHP.Prelude
|
||||
import IHP.ControllerPrelude
|
||||
|
||||
instance Controller TypeRegistriesController where
|
||||
beforeAction = ensureIsUser
|
||||
|
||||
-- ── Widget Types ─────────────────────────────────────────────────────────
|
||||
|
||||
action WidgetTypeRegistryAction = do
|
||||
entries <- query @WidgetTypeRegistry
|
||||
|> orderByAsc #label
|
||||
|> fetch
|
||||
hubs <- query @Hub |> fetch
|
||||
render WidgetTypesView { entries, hubs }
|
||||
|
||||
action ShowWidgetTypeAction { widgetTypeRegistryId } = do
|
||||
entry <- fetch widgetTypeRegistryId
|
||||
mOwner <- case entry.ownerHubId of
|
||||
Nothing -> pure Nothing
|
||||
Just hid -> fmap Just (fetch hid)
|
||||
render ShowWidgetTypeView { entry, mOwner }
|
||||
|
||||
action NewWidgetTypeAction = do
|
||||
let entry = newRecord @WidgetTypeRegistry
|
||||
hubs <- query @Hub |> fetch
|
||||
render NewWidgetTypeView { entry, hubs }
|
||||
|
||||
action CreateWidgetTypeAction = do
|
||||
let entry = newRecord @WidgetTypeRegistry
|
||||
hubs <- query @Hub |> fetch
|
||||
entry
|
||||
|> fill @'["name", "label", "description", "ownerHubId"]
|
||||
|> validateField #name nonEmpty
|
||||
|> validateField #label nonEmpty
|
||||
|> ifValid \case
|
||||
Left entry -> render NewWidgetTypeView { entry, hubs }
|
||||
Right entry -> do
|
||||
createRecord entry
|
||||
setSuccessMessage ("Widget type '" <> entry.name <> "' registered")
|
||||
redirectTo WidgetTypeRegistryAction
|
||||
|
||||
action EditWidgetTypeAction { widgetTypeRegistryId } = do
|
||||
entry <- fetch widgetTypeRegistryId
|
||||
hubs <- query @Hub |> fetch
|
||||
render EditWidgetTypeView { entry, hubs }
|
||||
|
||||
action UpdateWidgetTypeAction { widgetTypeRegistryId } = do
|
||||
entry <- fetch widgetTypeRegistryId
|
||||
hubs <- query @Hub |> fetch
|
||||
-- name is immutable after creation
|
||||
entry
|
||||
|> fill @'["label", "description", "ownerHubId"]
|
||||
|> validateField #label nonEmpty
|
||||
|> ifValid \case
|
||||
Left entry -> render EditWidgetTypeView { entry, hubs }
|
||||
Right entry -> do
|
||||
updateRecord entry
|
||||
setSuccessMessage "Widget type updated"
|
||||
redirectTo WidgetTypeRegistryAction
|
||||
|
||||
action DeprecateWidgetTypeAction { widgetTypeRegistryId } = do
|
||||
entry <- fetch widgetTypeRegistryId
|
||||
replacedBy <- param @Text "deprecated_in_favour_of"
|
||||
when (null replacedBy) do
|
||||
setErrorMessage "You must specify the replacement type name"
|
||||
redirectTo WidgetTypeRegistryAction
|
||||
entry
|
||||
|> set #status "deprecated"
|
||||
|> set #deprecatedInFavourOf (Just replacedBy)
|
||||
|> updateRecord
|
||||
setSuccessMessage ("Widget type '" <> entry.name <> "' deprecated")
|
||||
redirectTo WidgetTypeRegistryAction
|
||||
|
||||
-- ── Event Types ──────────────────────────────────────────────────────────
|
||||
|
||||
action EventTypeRegistryAction = do
|
||||
entries <- query @EventTypeRegistry
|
||||
|> orderByAsc #label
|
||||
|> fetch
|
||||
hubs <- query @Hub |> fetch
|
||||
render EventTypesView { entries, hubs }
|
||||
|
||||
action ShowEventTypeAction { eventTypeRegistryId } = do
|
||||
entry <- fetch eventTypeRegistryId
|
||||
mOwner <- case entry.ownerHubId of
|
||||
Nothing -> pure Nothing
|
||||
Just hid -> fmap Just (fetch hid)
|
||||
render ShowEventTypeView { entry, mOwner }
|
||||
|
||||
action NewEventTypeAction = do
|
||||
let entry = newRecord @EventTypeRegistry
|
||||
hubs <- query @Hub |> fetch
|
||||
render NewEventTypeView { entry, hubs }
|
||||
|
||||
action CreateEventTypeAction = do
|
||||
let entry = newRecord @EventTypeRegistry
|
||||
hubs <- query @Hub |> fetch
|
||||
entry
|
||||
|> fill @'["name", "label", "description", "ownerHubId"]
|
||||
|> validateField #name nonEmpty
|
||||
|> validateField #label nonEmpty
|
||||
|> ifValid \case
|
||||
Left entry -> render NewEventTypeView { entry, hubs }
|
||||
Right entry -> do
|
||||
createRecord entry
|
||||
setSuccessMessage ("Event type '" <> entry.name <> "' registered")
|
||||
redirectTo EventTypeRegistryAction
|
||||
|
||||
action EditEventTypeAction { eventTypeRegistryId } = do
|
||||
entry <- fetch eventTypeRegistryId
|
||||
hubs <- query @Hub |> fetch
|
||||
render EditEventTypeView { entry, hubs }
|
||||
|
||||
action UpdateEventTypeAction { eventTypeRegistryId } = do
|
||||
entry <- fetch eventTypeRegistryId
|
||||
hubs <- query @Hub |> fetch
|
||||
entry
|
||||
|> fill @'["label", "description", "ownerHubId"]
|
||||
|> validateField #label nonEmpty
|
||||
|> ifValid \case
|
||||
Left entry -> render EditEventTypeView { entry, hubs }
|
||||
Right entry -> do
|
||||
updateRecord entry
|
||||
setSuccessMessage "Event type updated"
|
||||
redirectTo EventTypeRegistryAction
|
||||
|
||||
action DeprecateEventTypeAction { eventTypeRegistryId } = do
|
||||
entry <- fetch eventTypeRegistryId
|
||||
replacedBy <- param @Text "deprecated_in_favour_of"
|
||||
when (null replacedBy) do
|
||||
setErrorMessage "You must specify the replacement type name"
|
||||
redirectTo EventTypeRegistryAction
|
||||
entry
|
||||
|> set #status "deprecated"
|
||||
|> set #deprecatedInFavourOf (Just replacedBy)
|
||||
|> updateRecord
|
||||
setSuccessMessage ("Event type '" <> entry.name <> "' deprecated")
|
||||
redirectTo EventTypeRegistryAction
|
||||
|
||||
-- ── Annotation Categories ────────────────────────────────────────────────
|
||||
|
||||
action AnnotationCategoryRegistryAction = do
|
||||
entries <- query @AnnotationCategoryRegistry
|
||||
|> orderByAsc #label
|
||||
|> fetch
|
||||
hubs <- query @Hub |> fetch
|
||||
render AnnotationCategoriesView { entries, hubs }
|
||||
|
||||
action ShowAnnotationCategoryAction { annotationCategoryRegistryId } = do
|
||||
entry <- fetch annotationCategoryRegistryId
|
||||
mOwner <- case entry.ownerHubId of
|
||||
Nothing -> pure Nothing
|
||||
Just hid -> fmap Just (fetch hid)
|
||||
render ShowAnnotationCategoryView { entry, mOwner }
|
||||
|
||||
action NewAnnotationCategoryAction = do
|
||||
let entry = newRecord @AnnotationCategoryRegistry
|
||||
hubs <- query @Hub |> fetch
|
||||
render NewAnnotationCategoryView { entry, hubs }
|
||||
|
||||
action CreateAnnotationCategoryAction = do
|
||||
let entry = newRecord @AnnotationCategoryRegistry
|
||||
hubs <- query @Hub |> fetch
|
||||
entry
|
||||
|> fill @'["name", "label", "description", "ownerHubId"]
|
||||
|> validateField #name nonEmpty
|
||||
|> validateField #label nonEmpty
|
||||
|> ifValid \case
|
||||
Left entry -> render NewAnnotationCategoryView { entry, hubs }
|
||||
Right entry -> do
|
||||
createRecord entry
|
||||
setSuccessMessage ("Annotation category '" <> entry.name <> "' registered")
|
||||
redirectTo AnnotationCategoryRegistryAction
|
||||
|
||||
action EditAnnotationCategoryAction { annotationCategoryRegistryId } = do
|
||||
entry <- fetch annotationCategoryRegistryId
|
||||
hubs <- query @Hub |> fetch
|
||||
render EditAnnotationCategoryView { entry, hubs }
|
||||
|
||||
action UpdateAnnotationCategoryAction { annotationCategoryRegistryId } = do
|
||||
entry <- fetch annotationCategoryRegistryId
|
||||
hubs <- query @Hub |> fetch
|
||||
entry
|
||||
|> fill @'["label", "description", "ownerHubId"]
|
||||
|> validateField #label nonEmpty
|
||||
|> ifValid \case
|
||||
Left entry -> render EditAnnotationCategoryView { entry, hubs }
|
||||
Right entry -> do
|
||||
updateRecord entry
|
||||
setSuccessMessage "Annotation category updated"
|
||||
redirectTo AnnotationCategoryRegistryAction
|
||||
|
||||
action DeprecateAnnotationCategoryAction { annotationCategoryRegistryId } = do
|
||||
entry <- fetch annotationCategoryRegistryId
|
||||
replacedBy <- param @Text "deprecated_in_favour_of"
|
||||
when (null replacedBy) do
|
||||
setErrorMessage "You must specify the replacement category name"
|
||||
redirectTo AnnotationCategoryRegistryAction
|
||||
entry
|
||||
|> set #status "deprecated"
|
||||
|> set #deprecatedInFavourOf (Just replacedBy)
|
||||
|> updateRecord
|
||||
setSuccessMessage ("Annotation category '" <> entry.name <> "' deprecated")
|
||||
redirectTo AnnotationCategoryRegistryAction
|
||||
|
||||
-- ── Policy Scopes ────────────────────────────────────────────────────────
|
||||
|
||||
action PolicyScopeRegistryAction = do
|
||||
entries <- query @PolicyScopeRegistry
|
||||
|> orderByAsc #label
|
||||
|> fetch
|
||||
hubs <- query @Hub |> fetch
|
||||
render PolicyScopesView { entries, hubs }
|
||||
|
||||
action ShowPolicyScopeAction { policyScopeRegistryId } = do
|
||||
entry <- fetch policyScopeRegistryId
|
||||
mOwner <- case entry.ownerHubId of
|
||||
Nothing -> pure Nothing
|
||||
Just hid -> fmap Just (fetch hid)
|
||||
render ShowPolicyScopeView { entry, mOwner }
|
||||
|
||||
action NewPolicyScopeAction = do
|
||||
let entry = newRecord @PolicyScopeRegistry
|
||||
hubs <- query @Hub |> fetch
|
||||
render NewPolicyScopeView { entry, hubs }
|
||||
|
||||
action CreatePolicyScopeAction = do
|
||||
let entry = newRecord @PolicyScopeRegistry
|
||||
hubs <- query @Hub |> fetch
|
||||
entry
|
||||
|> fill @'["name", "label", "description", "ownerHubId"]
|
||||
|> validateField #name nonEmpty
|
||||
|> validateField #label nonEmpty
|
||||
|> ifValid \case
|
||||
Left entry -> render NewPolicyScopeView { entry, hubs }
|
||||
Right entry -> do
|
||||
createRecord entry
|
||||
setSuccessMessage ("Policy scope '" <> entry.name <> "' registered")
|
||||
redirectTo PolicyScopeRegistryAction
|
||||
|
||||
action EditPolicyScopeAction { policyScopeRegistryId } = do
|
||||
entry <- fetch policyScopeRegistryId
|
||||
hubs <- query @Hub |> fetch
|
||||
render EditPolicyScopeView { entry, hubs }
|
||||
|
||||
action UpdatePolicyScopeAction { policyScopeRegistryId } = do
|
||||
entry <- fetch policyScopeRegistryId
|
||||
hubs <- query @Hub |> fetch
|
||||
entry
|
||||
|> fill @'["label", "description", "ownerHubId"]
|
||||
|> validateField #label nonEmpty
|
||||
|> ifValid \case
|
||||
Left entry -> render EditPolicyScopeView { entry, hubs }
|
||||
Right entry -> do
|
||||
updateRecord entry
|
||||
setSuccessMessage "Policy scope updated"
|
||||
redirectTo PolicyScopeRegistryAction
|
||||
|
||||
action DeprecatePolicyScopeAction { policyScopeRegistryId } = do
|
||||
entry <- fetch policyScopeRegistryId
|
||||
replacedBy <- param @Text "deprecated_in_favour_of"
|
||||
when (null replacedBy) do
|
||||
setErrorMessage "You must specify the replacement scope name"
|
||||
redirectTo PolicyScopeRegistryAction
|
||||
entry
|
||||
|> set #status "deprecated"
|
||||
|> set #deprecatedInFavourOf (Just replacedBy)
|
||||
|> updateRecord
|
||||
setSuccessMessage ("Policy scope '" <> entry.name <> "' deprecated")
|
||||
redirectTo PolicyScopeRegistryAction
|
||||
@@ -10,6 +10,7 @@ import IHP.Prelude
|
||||
import IHP.ControllerPrelude
|
||||
import Data.Aeson (toJSON, object, (.=))
|
||||
import Application.Helper.Controller (isInRegression, widgetCycleCounts, callClaudeApi)
|
||||
import Application.Helper.TypeRegistry (validateWidgetType, validatePolicyScope, activeWidgetTypes, activePolicyScopes)
|
||||
import Data.List (intercalate)
|
||||
|
||||
instance Controller WidgetsController where
|
||||
@@ -27,7 +28,9 @@ instance Controller WidgetsController where
|
||||
|> filterWhere (#status, "active")
|
||||
|> orderByAsc #name
|
||||
|> fetch
|
||||
render NewView { widget, hubs, adapterSpecs }
|
||||
(fwTypes, ownedTypes) <- activeWidgetTypes
|
||||
policyScopes <- activePolicyScopes
|
||||
render NewView { widget, hubs, adapterSpecs, widgetTypes = fwTypes <> ownedTypes, policyScopes }
|
||||
|
||||
action ShowWidgetAction { widgetId } = do
|
||||
widget <- fetch widgetId
|
||||
@@ -70,12 +73,27 @@ instance Controller WidgetsController where
|
||||
let widget = newRecord @Widget
|
||||
hubs <- query @Hub |> fetch
|
||||
adapterSpecs <- query @WidgetAdapterSpec |> filterWhere (#status, "active") |> orderByAsc #name |> fetch
|
||||
(fwTypes, ownedTypes) <- activeWidgetTypes
|
||||
policyScopes <- activePolicyScopes
|
||||
let widgetTypes = fwTypes <> ownedTypes
|
||||
widgetTypeVal <- paramOrDefault @Text "" "widgetType" >>= \t -> liftIO (validateWidgetType t)
|
||||
mPolicyScope <- paramOrNothing @Text "policyScope"
|
||||
policyScopeVal <- case mPolicyScope of
|
||||
Nothing -> pure (Right ())
|
||||
Just "" -> pure (Right ())
|
||||
Just ps -> liftIO (validatePolicyScope ps)
|
||||
widget
|
||||
|> fill @'["name", "widgetType", "hubId", "capabilityRef", "viewContext", "policyScope", "status", "adapterSpecId"]
|
||||
|> validateField #name nonEmpty
|
||||
|> validateField #widgetType nonEmpty
|
||||
|> (case widgetTypeVal of
|
||||
Left msg -> attachFailure #widgetType msg
|
||||
Right () -> id)
|
||||
|> (case policyScopeVal of
|
||||
Left msg -> attachFailure #policyScope msg
|
||||
Right () -> id)
|
||||
|> ifValid \case
|
||||
Left widget -> render NewView { widget, hubs, adapterSpecs }
|
||||
Left widget -> render NewView { widget, hubs, adapterSpecs, widgetTypes, policyScopes }
|
||||
Right widget -> do
|
||||
widget <- createRecord widget
|
||||
let snapshot = object
|
||||
@@ -100,18 +118,35 @@ instance Controller WidgetsController where
|
||||
widget <- fetch widgetId
|
||||
hubs <- query @Hub |> fetch
|
||||
adapterSpecs <- query @WidgetAdapterSpec |> filterWhere (#status, "active") |> orderByAsc #name |> fetch
|
||||
render EditView { widget, hubs, adapterSpecs }
|
||||
(fwTypes, ownedTypes) <- activeWidgetTypes
|
||||
policyScopes <- activePolicyScopes
|
||||
render EditView { widget, hubs, adapterSpecs, widgetTypes = fwTypes <> ownedTypes, policyScopes }
|
||||
|
||||
action UpdateWidgetAction { widgetId } = do
|
||||
widget <- fetch widgetId
|
||||
hubs <- query @Hub |> fetch
|
||||
adapterSpecs <- query @WidgetAdapterSpec |> filterWhere (#status, "active") |> orderByAsc #name |> fetch
|
||||
(fwTypes, ownedTypes) <- activeWidgetTypes
|
||||
policyScopes <- activePolicyScopes
|
||||
let widgetTypes = fwTypes <> ownedTypes
|
||||
widgetTypeVal <- paramOrDefault @Text "" "widgetType" >>= \t -> liftIO (validateWidgetType t)
|
||||
mPolicyScope <- paramOrNothing @Text "policyScope"
|
||||
policyScopeVal <- case mPolicyScope of
|
||||
Nothing -> pure (Right ())
|
||||
Just "" -> pure (Right ())
|
||||
Just ps -> liftIO (validatePolicyScope ps)
|
||||
widget
|
||||
|> fill @'["name", "widgetType", "hubId", "capabilityRef", "viewContext", "policyScope", "status", "adapterSpecId"]
|
||||
|> validateField #name nonEmpty
|
||||
|> validateField #widgetType nonEmpty
|
||||
|> (case widgetTypeVal of
|
||||
Left msg -> attachFailure #widgetType msg
|
||||
Right () -> id)
|
||||
|> (case policyScopeVal of
|
||||
Left msg -> attachFailure #policyScope msg
|
||||
Right () -> id)
|
||||
|> ifValid \case
|
||||
Left widget -> render EditView { widget, hubs, adapterSpecs }
|
||||
Left widget -> render EditView { widget, hubs, adapterSpecs, widgetTypes, policyScopes }
|
||||
Right widget -> do
|
||||
let newVersion = widget.version + 1
|
||||
widget <- widget |> set #version newVersion |> updateRecord
|
||||
|
||||
@@ -30,6 +30,8 @@ import Web.Controller.FederatedPolicyOverlays ()
|
||||
import Web.Controller.StewardshipRoles ()
|
||||
import Web.Controller.ArchiveRecords ()
|
||||
import Web.Controller.FederatedGovernance ()
|
||||
import Web.Controller.TypeRegistries ()
|
||||
import Web.Controller.HubCapabilityManifests ()
|
||||
import Web.Controller.Sessions ()
|
||||
|
||||
instance FrontController WebApplication where
|
||||
@@ -56,6 +58,8 @@ instance FrontController WebApplication where
|
||||
, parseRoute @StewardshipRolesController
|
||||
, parseRoute @ArchiveRecordsController
|
||||
, parseRoute @FederatedGovernanceController
|
||||
, parseRoute @TypeRegistriesController
|
||||
, parseRoute @HubCapabilityManifestsController
|
||||
]
|
||||
|
||||
instance InitControllerContext WebApplication where
|
||||
@@ -100,6 +104,8 @@ defaultLayout inner = [hsx|
|
||||
<a href={FederatedGovernanceDashboardAction} class="text-sm text-gray-600 hover:text-gray-900">Federation</a>
|
||||
<a href={FederatedPolicyOverlaysAction} class="text-sm text-gray-600 hover:text-gray-900">Policies</a>
|
||||
<a href={ArchiveRecordsAction} class="text-sm text-gray-600 hover:text-gray-900">Archive</a>
|
||||
<a href={WidgetTypeRegistryAction} class="text-sm text-gray-600 hover:text-gray-900">Registries</a>
|
||||
<a href={HubCapabilityManifestsAction} class="text-sm text-gray-600 hover:text-gray-900">Extensions</a>
|
||||
<div class="ml-auto">
|
||||
<a href={DeleteSessionAction} class="text-sm text-gray-500 hover:text-gray-700">Sign out</a>
|
||||
</div>
|
||||
|
||||
@@ -63,5 +63,9 @@ instance AutoRoute StewardshipRolesController
|
||||
instance AutoRoute ArchiveRecordsController
|
||||
instance AutoRoute FederatedGovernanceController
|
||||
|
||||
-- GAAF Compliance Foundation (IHUB-WP-0009)
|
||||
instance AutoRoute TypeRegistriesController
|
||||
instance AutoRoute HubCapabilityManifestsController
|
||||
|
||||
-- Sessions
|
||||
instance AutoRoute SessionsController
|
||||
|
||||
44
Web/Types.hs
44
Web/Types.hs
@@ -205,6 +205,50 @@ data CrossHubPropagationsController
|
||||
| ResolvePropagationAction { crossHubPropagationId :: !(Id CrossHubPropagation) }
|
||||
deriving (Eq, Show, Data)
|
||||
|
||||
-- GAAF Compliance Foundation (IHUB-WP-0009)
|
||||
|
||||
data TypeRegistriesController
|
||||
= WidgetTypeRegistryAction
|
||||
| ShowWidgetTypeAction { widgetTypeRegistryId :: !(Id WidgetTypeRegistry) }
|
||||
| NewWidgetTypeAction
|
||||
| CreateWidgetTypeAction
|
||||
| EditWidgetTypeAction { widgetTypeRegistryId :: !(Id WidgetTypeRegistry) }
|
||||
| UpdateWidgetTypeAction { widgetTypeRegistryId :: !(Id WidgetTypeRegistry) }
|
||||
| DeprecateWidgetTypeAction { widgetTypeRegistryId :: !(Id WidgetTypeRegistry) }
|
||||
| EventTypeRegistryAction
|
||||
| ShowEventTypeAction { eventTypeRegistryId :: !(Id EventTypeRegistry) }
|
||||
| NewEventTypeAction
|
||||
| CreateEventTypeAction
|
||||
| EditEventTypeAction { eventTypeRegistryId :: !(Id EventTypeRegistry) }
|
||||
| UpdateEventTypeAction { eventTypeRegistryId :: !(Id EventTypeRegistry) }
|
||||
| DeprecateEventTypeAction { eventTypeRegistryId :: !(Id EventTypeRegistry) }
|
||||
| AnnotationCategoryRegistryAction
|
||||
| ShowAnnotationCategoryAction { annotationCategoryRegistryId :: !(Id AnnotationCategoryRegistry) }
|
||||
| NewAnnotationCategoryAction
|
||||
| CreateAnnotationCategoryAction
|
||||
| EditAnnotationCategoryAction { annotationCategoryRegistryId :: !(Id AnnotationCategoryRegistry) }
|
||||
| UpdateAnnotationCategoryAction { annotationCategoryRegistryId :: !(Id AnnotationCategoryRegistry) }
|
||||
| DeprecateAnnotationCategoryAction { annotationCategoryRegistryId :: !(Id AnnotationCategoryRegistry) }
|
||||
| PolicyScopeRegistryAction
|
||||
| ShowPolicyScopeAction { policyScopeRegistryId :: !(Id PolicyScopeRegistry) }
|
||||
| NewPolicyScopeAction
|
||||
| CreatePolicyScopeAction
|
||||
| EditPolicyScopeAction { policyScopeRegistryId :: !(Id PolicyScopeRegistry) }
|
||||
| UpdatePolicyScopeAction { policyScopeRegistryId :: !(Id PolicyScopeRegistry) }
|
||||
| DeprecatePolicyScopeAction { policyScopeRegistryId :: !(Id PolicyScopeRegistry) }
|
||||
deriving (Eq, Show, Data)
|
||||
|
||||
data HubCapabilityManifestsController
|
||||
= HubCapabilityManifestsAction
|
||||
| ShowHubCapabilityManifestAction { hubCapabilityManifestId :: !(Id HubCapabilityManifest) }
|
||||
| NewHubCapabilityManifestAction
|
||||
| CreateHubCapabilityManifestAction
|
||||
| EditHubCapabilityManifestAction { hubCapabilityManifestId :: !(Id HubCapabilityManifest) }
|
||||
| UpdateHubCapabilityManifestAction { hubCapabilityManifestId :: !(Id HubCapabilityManifest) }
|
||||
| ActivateManifestAction { hubCapabilityManifestId :: !(Id HubCapabilityManifest) }
|
||||
| RetireManifestAction { hubCapabilityManifestId :: !(Id HubCapabilityManifest) }
|
||||
deriving (Eq, Show, Data)
|
||||
|
||||
data SessionsController
|
||||
= NewSessionAction
|
||||
| CreateSessionAction
|
||||
|
||||
@@ -8,6 +8,7 @@ import IHP.ViewPrelude
|
||||
data NewView = NewView
|
||||
{ widget :: !Widget
|
||||
, annotation :: !Annotation
|
||||
, categories :: ![AnnotationCategoryRegistry]
|
||||
}
|
||||
|
||||
instance View NewView where
|
||||
@@ -21,28 +22,20 @@ instance View NewView where
|
||||
<span>New</span>
|
||||
</div>
|
||||
<h1 class="text-2xl font-semibold mb-6">Add Annotation</h1>
|
||||
{renderForm annotation widget.id}
|
||||
{renderForm annotation widget.id categories}
|
||||
</div>
|
||||
|]
|
||||
|
||||
renderForm :: Annotation -> Id Widget -> Html
|
||||
renderForm annotation widgetId = formFor annotation [hsx|
|
||||
renderForm :: Annotation -> Id Widget -> [AnnotationCategoryRegistry] -> Html
|
||||
renderForm annotation widgetId categories = formFor annotation [hsx|
|
||||
{(textareaField #body) { fieldLabel = "Comment" }}
|
||||
{selectField #category categoryOptions}
|
||||
{selectField #category (categoryOptions categories)}
|
||||
{selectField #severity severityOptions}
|
||||
{submitButton}
|
||||
|]
|
||||
|
||||
categoryOptions :: [(Text, Text)]
|
||||
categoryOptions =
|
||||
[ ("Friction", "friction")
|
||||
, ("Defect", "defect")
|
||||
, ("Wish", "wish")
|
||||
, ("Policy Concern", "policy_concern")
|
||||
, ("Documentation Gap", "doc_gap")
|
||||
, ("Trust", "trust")
|
||||
, ("Other", "other")
|
||||
]
|
||||
categoryOptions :: [AnnotationCategoryRegistry] -> [(Text, Text)]
|
||||
categoryOptions = map (\r -> (r.label, r.name))
|
||||
|
||||
severityOptions :: [(Text, Text)]
|
||||
severityOptions =
|
||||
|
||||
@@ -25,6 +25,7 @@ instance View ShowView where
|
||||
<span class={adapterStatusBadge contract.status <> " text-xs px-2 py-0.5 rounded font-medium"}>
|
||||
{contract.status}
|
||||
</span>
|
||||
{maturityBadge contract.maturity}
|
||||
</div>
|
||||
|
||||
{forEach (contractDescription contract) (\d -> [hsx|
|
||||
@@ -57,3 +58,10 @@ contractDescription :: EnvelopeEmissionContract -> [Text]
|
||||
contractDescription c = case c.description of
|
||||
Just d -> [d]
|
||||
Nothing -> []
|
||||
|
||||
maturityBadge :: Text -> Html
|
||||
maturityBadge "stable" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-green-100 text-green-800 font-medium">Stable</span>|]
|
||||
maturityBadge "beta" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-blue-100 text-blue-800 font-medium">Beta</span>|]
|
||||
maturityBadge "experimental" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-amber-100 text-amber-800 font-medium">Experimental</span>|]
|
||||
maturityBadge "deprecated" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-500 font-medium">Deprecated</span>|]
|
||||
maturityBadge m = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-600 font-medium">{m}</span>|]
|
||||
|
||||
130
Web/View/HubCapabilityManifests/Edit.hs
Normal file
130
Web/View/HubCapabilityManifests/Edit.hs
Normal file
@@ -0,0 +1,130 @@
|
||||
module Web.View.HubCapabilityManifests.Edit where
|
||||
|
||||
import Web.Types
|
||||
import Generated.Types
|
||||
import IHP.Prelude
|
||||
import IHP.ViewPrelude
|
||||
import Data.Aeson (Value(..), encode, decode)
|
||||
import qualified Data.Vector as V
|
||||
import qualified Data.ByteString.Lazy.Char8 as BL
|
||||
|
||||
data EditView = EditView
|
||||
{ manifest :: !HubCapabilityManifest
|
||||
, hub :: !Hub
|
||||
, widgetTypeEntries :: ![WidgetTypeRegistry]
|
||||
, eventTypeEntries :: ![EventTypeRegistry]
|
||||
, categoryEntries :: ![AnnotationCategoryRegistry]
|
||||
, policyScopeEntries :: ![PolicyScopeRegistry]
|
||||
}
|
||||
|
||||
instance View EditView where
|
||||
html EditView { .. } = [hsx|
|
||||
<div class="mb-4">
|
||||
<a href={ShowHubCapabilityManifestAction { hubCapabilityManifestId = manifest.id }}
|
||||
class="text-sm text-gray-500 hover:text-gray-700">
|
||||
← {hub.name} Manifest
|
||||
</a>
|
||||
</div>
|
||||
<h1 class="text-xl font-semibold mb-2">Edit Capability Manifest — {hub.name}</h1>
|
||||
<p class="text-sm text-gray-500 mb-6">
|
||||
Declare the type names this hub owns. After saving, activate the manifest to register them.
|
||||
</p>
|
||||
|
||||
{if manifest.status /= "draft"
|
||||
then [hsx|
|
||||
<div class="mb-6 bg-amber-50 border border-amber-200 rounded p-4 text-sm text-amber-800">
|
||||
This manifest is <strong>{manifest.status}</strong> and is read-only.
|
||||
Retire it first to create a new draft amendment.
|
||||
</div>
|
||||
|]
|
||||
else [hsx||]}
|
||||
|
||||
<form method="POST" action={UpdateHubCapabilityManifestAction { hubCapabilityManifestId = manifest.id }}>
|
||||
<div class="space-y-6 max-w-2xl">
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-5 space-y-4">
|
||||
<h2 class="text-sm font-semibold text-gray-700">Manifest Details</h2>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Capability Description</label>
|
||||
{(textareaField #capabilityDescription) { fieldClass = "w-full border border-gray-300 rounded px-3 py-2 text-sm" }}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Contact</label>
|
||||
{(textField #contact) { fieldClass = "w-full border border-gray-300 rounded px-3 py-2 text-sm" }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{typeArraySection "Declared Widget Types" "declaredWidgetTypes" manifest.declaredWidgetTypes widgetTypeEntries}
|
||||
{typeArraySection "Declared Event Types" "declaredEventTypes" manifest.declaredEventTypes eventTypeEntries}
|
||||
{typeArraySection2 "Declared Annotation Categories" "declaredAnnotationCategories" manifest.declaredAnnotationCategories categoryEntries}
|
||||
{typeArraySection3 "Declared Policy Scopes" "declaredPolicyScopes" manifest.declaredPolicyScopes policyScopeEntries}
|
||||
|
||||
<div class="flex gap-3">
|
||||
<button type="submit"
|
||||
class="bg-indigo-600 text-white text-sm px-4 py-2 rounded hover:bg-indigo-700"
|
||||
{if manifest.status /= "draft" then ("disabled" :: Text) else ""}>
|
||||
Save
|
||||
</button>
|
||||
{if manifest.status == "draft" then [hsx|
|
||||
<a href={ActivateManifestAction { hubCapabilityManifestId = manifest.id }}
|
||||
class="text-sm bg-green-600 text-white px-4 py-2 rounded hover:bg-green-700">
|
||||
Save & Activate
|
||||
</a>
|
||||
|] else [hsx||]}
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|]
|
||||
|
||||
-- | Render a JSON array text area with available registry options shown below.
|
||||
typeArraySection :: Text -> Text -> Value -> [WidgetTypeRegistry] -> Html
|
||||
typeArraySection title fieldName val entries = [hsx|
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-5">
|
||||
<h2 class="text-sm font-semibold text-gray-700 mb-1">{title}</h2>
|
||||
<p class="text-xs text-gray-500 mb-2">
|
||||
JSON array of type names to declare ownership of.
|
||||
Names that don't yet exist in the registry will be created on activation.
|
||||
</p>
|
||||
<textarea name={fieldName}
|
||||
class="w-full border border-gray-300 rounded px-3 py-2 text-sm font-mono"
|
||||
rows="3">{valueText val}</textarea>
|
||||
<p class="text-xs text-gray-400 mt-1">
|
||||
Registered: {intercalate ", " (map (.name) entries)}
|
||||
</p>
|
||||
</div>
|
||||
|]
|
||||
|
||||
typeArraySection2 :: Text -> Text -> Value -> [AnnotationCategoryRegistry] -> Html
|
||||
typeArraySection2 title fieldName val entries = [hsx|
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-5">
|
||||
<h2 class="text-sm font-semibold text-gray-700 mb-1">{title}</h2>
|
||||
<p class="text-xs text-gray-500 mb-2">JSON array of annotation category names.</p>
|
||||
<textarea name={fieldName}
|
||||
class="w-full border border-gray-300 rounded px-3 py-2 text-sm font-mono"
|
||||
rows="3">{valueText val}</textarea>
|
||||
<p class="text-xs text-gray-400 mt-1">
|
||||
Registered: {intercalate ", " (map (.name) entries)}
|
||||
</p>
|
||||
</div>
|
||||
|]
|
||||
|
||||
typeArraySection3 :: Text -> Text -> Value -> [PolicyScopeRegistry] -> Html
|
||||
typeArraySection3 title fieldName val entries = [hsx|
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-5">
|
||||
<h2 class="text-sm font-semibold text-gray-700 mb-1">{title}</h2>
|
||||
<p class="text-xs text-gray-500 mb-2">JSON array of policy scope names.</p>
|
||||
<textarea name={fieldName}
|
||||
class="w-full border border-gray-300 rounded px-3 py-2 text-sm font-mono"
|
||||
rows="3">{valueText val}</textarea>
|
||||
<p class="text-xs text-gray-400 mt-1">
|
||||
Registered: {intercalate ", " (map (.name) entries)}
|
||||
</p>
|
||||
</div>
|
||||
|]
|
||||
|
||||
valueText :: Value -> Text
|
||||
valueText v = cs (BL.unpack (encode v))
|
||||
|
||||
intercalate :: Text -> [Text] -> Text
|
||||
intercalate _ [] = ""
|
||||
intercalate _ [x] = x
|
||||
intercalate sep (x:xs) = x <> sep <> intercalate sep xs
|
||||
77
Web/View/HubCapabilityManifests/Index.hs
Normal file
77
Web/View/HubCapabilityManifests/Index.hs
Normal file
@@ -0,0 +1,77 @@
|
||||
module Web.View.HubCapabilityManifests.Index where
|
||||
|
||||
import Web.Types
|
||||
import Generated.Types
|
||||
import IHP.Prelude
|
||||
import IHP.ViewPrelude
|
||||
import Data.Aeson (Value(..))
|
||||
import qualified Data.Vector as V
|
||||
|
||||
data IndexView = IndexView
|
||||
{ manifests :: ![HubCapabilityManifest]
|
||||
, hubs :: ![Hub]
|
||||
}
|
||||
|
||||
instance View IndexView where
|
||||
html IndexView { .. } = [hsx|
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold">Hub Capability Manifests</h1>
|
||||
<p class="text-sm text-gray-500 mt-1">Extension registrations for domain and shared hubs</p>
|
||||
</div>
|
||||
<a href={NewHubCapabilityManifestAction}
|
||||
class="bg-indigo-600 text-white text-sm font-medium px-4 py-2 rounded hover:bg-indigo-700">
|
||||
New Manifest
|
||||
</a>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50 border-b border-gray-200">
|
||||
<tr>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-700">Hub</th>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-700">Status</th>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-700">Widget Types</th>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-700">Event Types</th>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-700">Categories</th>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-700">Scopes</th>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-700">Activated</th>
|
||||
<th class="px-4 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{forEach manifests (renderRow hubs)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|]
|
||||
|
||||
renderRow :: [Hub] -> HubCapabilityManifest -> Html
|
||||
renderRow hubs m = [hsx|
|
||||
<tr class="border-b border-gray-100 hover:bg-gray-50">
|
||||
<td class="px-4 py-3 font-medium">{hubName hubs m.hubId}</td>
|
||||
<td class="px-4 py-3">{statusBadge m.status}</td>
|
||||
<td class="px-4 py-3 text-gray-500 text-xs">{jsonCount m.declaredWidgetTypes}</td>
|
||||
<td class="px-4 py-3 text-gray-500 text-xs">{jsonCount m.declaredEventTypes}</td>
|
||||
<td class="px-4 py-3 text-gray-500 text-xs">{jsonCount m.declaredAnnotationCategories}</td>
|
||||
<td class="px-4 py-3 text-gray-500 text-xs">{jsonCount m.declaredPolicyScopes}</td>
|
||||
<td class="px-4 py-3 text-gray-400 text-xs">{maybe "—" show m.activatedAt}</td>
|
||||
<td class="px-4 py-3 text-right text-xs">
|
||||
<a href={ShowHubCapabilityManifestAction { hubCapabilityManifestId = m.id }}
|
||||
class="text-indigo-600 hover:text-indigo-800">View</a>
|
||||
</td>
|
||||
</tr>
|
||||
|]
|
||||
|
||||
hubName :: [Hub] -> Id Hub -> Text
|
||||
hubName hubs i = maybe "Unknown" (.name) (find (\h -> h.id == i) hubs)
|
||||
|
||||
statusBadge :: Text -> Html
|
||||
statusBadge "active" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-green-100 text-green-800">active</span>|]
|
||||
statusBadge "draft" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-amber-100 text-amber-800">draft</span>|]
|
||||
statusBadge "retired" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-500">retired</span>|]
|
||||
statusBadge s = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-600">{s}</span>|]
|
||||
|
||||
jsonCount :: Value -> Text
|
||||
jsonCount (Array v) | V.null v = "0"
|
||||
| otherwise = tshow (V.length v)
|
||||
jsonCount _ = "0"
|
||||
55
Web/View/HubCapabilityManifests/New.hs
Normal file
55
Web/View/HubCapabilityManifests/New.hs
Normal file
@@ -0,0 +1,55 @@
|
||||
module Web.View.HubCapabilityManifests.New where
|
||||
|
||||
import Web.Types
|
||||
import Generated.Types
|
||||
import IHP.Prelude
|
||||
import IHP.ViewPrelude
|
||||
|
||||
data NewView = NewView
|
||||
{ manifest :: !HubCapabilityManifest
|
||||
, hubs :: ![Hub]
|
||||
}
|
||||
|
||||
instance View NewView where
|
||||
html NewView { .. } = [hsx|
|
||||
<div class="mb-4">
|
||||
<a href={HubCapabilityManifestsAction} class="text-sm text-gray-500 hover:text-gray-700">
|
||||
← Capability Manifests
|
||||
</a>
|
||||
</div>
|
||||
<h1 class="text-xl font-semibold mb-6">New Capability Manifest</h1>
|
||||
<div class="bg-amber-50 border border-amber-200 rounded p-4 mb-6 text-sm text-amber-800">
|
||||
A capability manifest lets a domain or shared hub declare the widget types, event types,
|
||||
annotation categories, and policy scopes it owns. Create a draft, declare your types,
|
||||
then activate to register them with the framework.
|
||||
</div>
|
||||
<form method="POST" action={CreateHubCapabilityManifestAction}>
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6 max-w-lg space-y-4">
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Hub</label>
|
||||
{selectField #hubId (hubOptions hubs)}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Capability Description <span class="text-gray-400 text-xs">(optional)</span>
|
||||
</label>
|
||||
{(textareaField #capabilityDescription) { fieldClass = "w-full border border-gray-300 rounded px-3 py-2 text-sm" }}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Contact <span class="text-gray-400 text-xs">(team or person)</span>
|
||||
</label>
|
||||
{(textField #contact) { fieldClass = "w-full border border-gray-300 rounded px-3 py-2 text-sm" }}
|
||||
</div>
|
||||
<div class="pt-2">
|
||||
<button type="submit"
|
||||
class="bg-indigo-600 text-white text-sm px-4 py-2 rounded hover:bg-indigo-700">
|
||||
Create Draft
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
|]
|
||||
|
||||
hubOptions :: [Hub] -> [(Text, Id Hub)]
|
||||
hubOptions hubs = map (\h -> (h.name <> " (" <> h.hubKind <> ")", h.id)) hubs
|
||||
116
Web/View/HubCapabilityManifests/Show.hs
Normal file
116
Web/View/HubCapabilityManifests/Show.hs
Normal file
@@ -0,0 +1,116 @@
|
||||
module Web.View.HubCapabilityManifests.Show where
|
||||
|
||||
import Web.Types
|
||||
import Generated.Types
|
||||
import IHP.Prelude
|
||||
import IHP.ViewPrelude
|
||||
import Data.Aeson (Value(..), encode)
|
||||
import qualified Data.Vector as V
|
||||
import qualified Data.ByteString.Lazy.Char8 as BL
|
||||
|
||||
data ShowView = ShowView
|
||||
{ manifest :: !HubCapabilityManifest
|
||||
, hub :: !Hub
|
||||
}
|
||||
|
||||
instance View ShowView where
|
||||
html ShowView { .. } = [hsx|
|
||||
<div class="mb-4">
|
||||
<a href={HubCapabilityManifestsAction} class="text-sm text-gray-500 hover:text-gray-700">
|
||||
← Capability Manifests
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<div class="flex items-center gap-3 mb-6">
|
||||
<h1 class="text-2xl font-semibold">{hub.name} — Capability Manifest</h1>
|
||||
{statusBadge manifest.status}
|
||||
</div>
|
||||
|
||||
{if manifest.status == "draft"
|
||||
then [hsx|
|
||||
<div class="mb-4 flex gap-2">
|
||||
<a href={EditHubCapabilityManifestAction { hubCapabilityManifestId = manifest.id }}
|
||||
class="text-sm border border-indigo-300 text-indigo-700 px-3 py-1.5 rounded hover:bg-indigo-50">
|
||||
Edit Draft
|
||||
</a>
|
||||
<a href={ActivateManifestAction { hubCapabilityManifestId = manifest.id }}
|
||||
class="text-sm bg-green-600 text-white px-3 py-1.5 rounded hover:bg-green-700">
|
||||
Activate
|
||||
</a>
|
||||
</div>
|
||||
|]
|
||||
else if manifest.status == "active"
|
||||
then [hsx|
|
||||
<div class="mb-4">
|
||||
<a href={RetireManifestAction { hubCapabilityManifestId = manifest.id }}
|
||||
data-confirm="Retire this manifest? The hub's types will remain registered."
|
||||
class="text-sm border border-gray-300 text-gray-600 px-3 py-1.5 rounded hover:bg-gray-50">
|
||||
Retire
|
||||
</a>
|
||||
</div>
|
||||
|]
|
||||
else [hsx||]}
|
||||
|
||||
<div class="grid grid-cols-2 gap-4 mb-6">
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-4">
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wide">Manifest Version</p>
|
||||
<p class="font-mono font-medium mt-1">{manifest.manifestVersion}</p>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-4">
|
||||
<p class="text-xs text-gray-500 uppercase tracking-wide">Activated</p>
|
||||
<p class="font-medium mt-1">{maybe "—" show manifest.activatedAt}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{forEach (maybeText manifest.capabilityDescription) (\d -> [hsx|
|
||||
<p class="text-sm text-gray-600 mb-4">{d}</p>
|
||||
|])}
|
||||
{forEach (maybeText manifest.contact) (\c -> [hsx|
|
||||
<p class="text-xs text-gray-400 mb-6">Contact: {c}</p>
|
||||
|])}
|
||||
|
||||
<div class="grid grid-cols-2 gap-4">
|
||||
{jsonArraySection "Declared Widget Types" manifest.declaredWidgetTypes}
|
||||
{jsonArraySection "Declared Event Types" manifest.declaredEventTypes}
|
||||
{jsonArraySection "Declared Annotation Categories" manifest.declaredAnnotationCategories}
|
||||
{jsonArraySection "Declared Policy Scopes" manifest.declaredPolicyScopes}
|
||||
</div>
|
||||
|]
|
||||
|
||||
jsonArraySection :: Text -> Value -> Html
|
||||
jsonArraySection title val = [hsx|
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-4">
|
||||
<h3 class="text-sm font-semibold text-gray-700 mb-2">{title}
|
||||
<span class="text-gray-400 font-normal ml-1">({arrayLen val})</span>
|
||||
</h3>
|
||||
{renderArrayItems val}
|
||||
</div>
|
||||
|]
|
||||
|
||||
renderArrayItems :: Value -> Html
|
||||
renderArrayItems (Array v) | V.null v =
|
||||
[hsx|<p class="text-xs text-gray-400">None declared</p>|]
|
||||
renderArrayItems (Array v) = [hsx|
|
||||
<ul class="space-y-1">
|
||||
{forEach (V.toList v) renderItem}
|
||||
</ul>
|
||||
|]
|
||||
renderArrayItems _ = [hsx|<p class="text-xs text-gray-400">—</p>|]
|
||||
|
||||
renderItem :: Value -> Html
|
||||
renderItem (String t) = [hsx|<li class="font-mono text-xs text-gray-700">{t}</li>|]
|
||||
renderItem v = [hsx|<li class="font-mono text-xs text-gray-500">{cs (BL.unpack (encode v)) :: Text}</li>|]
|
||||
|
||||
arrayLen :: Value -> Text
|
||||
arrayLen (Array v) = tshow (V.length v)
|
||||
arrayLen _ = "0"
|
||||
|
||||
statusBadge :: Text -> Html
|
||||
statusBadge "active" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-green-100 text-green-800">active</span>|]
|
||||
statusBadge "draft" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-amber-100 text-amber-800">draft</span>|]
|
||||
statusBadge "retired" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-500">retired</span>|]
|
||||
statusBadge s = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-600">{s}</span>|]
|
||||
|
||||
maybeText :: Maybe Text -> [Text]
|
||||
maybeText Nothing = []
|
||||
maybeText (Just t) = [t]
|
||||
@@ -24,6 +24,7 @@ instance View IndexView where
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-700">Name</th>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-700">Slug</th>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-700">Domain</th>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-700">Kind</th>
|
||||
<th class="px-4 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
@@ -34,6 +35,11 @@ instance View IndexView where
|
||||
</div>
|
||||
|]
|
||||
|
||||
kindBadge :: Text -> Html
|
||||
kindBadge "framework" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-purple-100 text-purple-800">framework</span>|]
|
||||
kindBadge "shared" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-teal-100 text-teal-800">shared</span>|]
|
||||
kindBadge _ = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-blue-100 text-blue-800">domain</span>|]
|
||||
|
||||
renderHub :: Hub -> Html
|
||||
renderHub hub = [hsx|
|
||||
<tr class="border-b border-gray-100 hover:bg-gray-50">
|
||||
@@ -45,6 +51,7 @@ renderHub hub = [hsx|
|
||||
</td>
|
||||
<td class="px-4 py-3 text-gray-500 font-mono text-xs">{hub.slug}</td>
|
||||
<td class="px-4 py-3 text-gray-500">{hub.domain}</td>
|
||||
<td class="px-4 py-3">{kindBadge hub.hubKind}</td>
|
||||
<td class="px-4 py-3 text-right">
|
||||
<a href={EditHubAction { hubId = hub.id }}
|
||||
class="text-gray-500 hover:text-gray-700 text-xs mr-3">Edit</a>
|
||||
|
||||
@@ -10,6 +10,7 @@ data ShowView = ShowView
|
||||
, widgets :: ![Widget]
|
||||
, recentEvents :: ![InteractionEvent]
|
||||
, recentAnnotations :: ![Annotation]
|
||||
, mManifest :: !(Maybe HubCapabilityManifest)
|
||||
}
|
||||
|
||||
instance View ShowView where
|
||||
@@ -22,7 +23,10 @@ instance View ShowView where
|
||||
</div>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold">{hub.name}</h1>
|
||||
<div class="flex items-center gap-2">
|
||||
<h1 class="text-2xl font-semibold">{hub.name}</h1>
|
||||
{kindBadge hub.hubKind}
|
||||
</div>
|
||||
<p class="text-sm text-gray-500 mt-1">
|
||||
<span class="font-mono bg-gray-100 px-1 rounded">{hub.slug}</span>
|
||||
<span class="ml-2">{hub.domain}</span>
|
||||
@@ -131,6 +135,11 @@ instance View ShowView where
|
||||
{forEach recentAnnotations renderAnnotationCard}
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="mt-8">
|
||||
<h2 class="text-lg font-medium mb-3">Capability Manifest</h2>
|
||||
{renderManifestSection mManifest hub.id}
|
||||
</section>
|
||||
|]
|
||||
|
||||
renderWidgetRow :: Widget -> Html
|
||||
@@ -171,3 +180,48 @@ renderAnnotationCard a = [hsx|
|
||||
<p class="text-sm text-gray-700">{a.body}</p>
|
||||
</div>
|
||||
|]
|
||||
|
||||
renderManifestSection :: Maybe HubCapabilityManifest -> Id Hub -> Html
|
||||
renderManifestSection Nothing hubId = [hsx|
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-5 flex items-center justify-between">
|
||||
<div>
|
||||
<p class="text-sm text-gray-600">No capability manifest registered for this hub.</p>
|
||||
<p class="text-xs text-gray-400 mt-1">
|
||||
Domain hubs should declare their vocabulary before creating hub-owned type registry entries.
|
||||
</p>
|
||||
</div>
|
||||
<a href={NewHubCapabilityManifestAction}
|
||||
class="text-sm border border-indigo-300 text-indigo-700 px-3 py-1.5 rounded hover:bg-indigo-50">
|
||||
Register Capabilities
|
||||
</a>
|
||||
</div>
|
||||
|]
|
||||
renderManifestSection (Just m) _ = [hsx|
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-5">
|
||||
<div class="flex items-center justify-between mb-3">
|
||||
<div class="flex items-center gap-2">
|
||||
{manifestStatusBadge m.status}
|
||||
<span class="text-sm text-gray-600">v{m.manifestVersion}</span>
|
||||
{forEach (maybeText m.capabilityDescription) (\d -> [hsx|<span class="text-sm text-gray-500">— {d}</span>|])}
|
||||
</div>
|
||||
<a href={ShowHubCapabilityManifestAction { hubCapabilityManifestId = m.id }}
|
||||
class="text-sm text-indigo-600 hover:text-indigo-800">View manifest →</a>
|
||||
</div>
|
||||
{forEach (maybeText m.contact) (\c -> [hsx|<p class="text-xs text-gray-400">Contact: {c}</p>|])}
|
||||
</div>
|
||||
|]
|
||||
|
||||
manifestStatusBadge :: Text -> Html
|
||||
manifestStatusBadge "active" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-green-100 text-green-800">active</span>|]
|
||||
manifestStatusBadge "draft" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-amber-100 text-amber-800">draft</span>|]
|
||||
manifestStatusBadge "retired" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-500">retired</span>|]
|
||||
manifestStatusBadge s = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-600">{s}</span>|]
|
||||
|
||||
kindBadge :: Text -> Html
|
||||
kindBadge "framework" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-purple-100 text-purple-800">framework</span>|]
|
||||
kindBadge "shared" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-teal-100 text-teal-800">shared</span>|]
|
||||
kindBadge _ = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-blue-100 text-blue-800">domain</span>|]
|
||||
|
||||
maybeText :: Maybe Text -> [Text]
|
||||
maybeText Nothing = []
|
||||
maybeText (Just t) = [t]
|
||||
|
||||
@@ -25,6 +25,7 @@ instance View ShowView where
|
||||
<span class={adapterStatusBadge contract.status <> " text-xs px-2 py-0.5 rounded font-medium"}>
|
||||
{contract.status}
|
||||
</span>
|
||||
{maturityBadge contract.maturity}
|
||||
</div>
|
||||
|
||||
{forEach (contractDescription contract) (\d -> [hsx|
|
||||
@@ -74,3 +75,10 @@ contractDescription :: InteractionReportingContract -> [Text]
|
||||
contractDescription c = case c.description of
|
||||
Just d -> [d]
|
||||
Nothing -> []
|
||||
|
||||
maturityBadge :: Text -> Html
|
||||
maturityBadge "stable" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-green-100 text-green-800 font-medium">Stable</span>|]
|
||||
maturityBadge "beta" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-blue-100 text-blue-800 font-medium">Beta</span>|]
|
||||
maturityBadge "experimental" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-amber-100 text-amber-800 font-medium">Experimental</span>|]
|
||||
maturityBadge "deprecated" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-500 font-medium">Deprecated</span>|]
|
||||
maturityBadge m = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-600 font-medium">{m}</span>|]
|
||||
|
||||
151
Web/View/TypeRegistries/AnnotationCategories.hs
Normal file
151
Web/View/TypeRegistries/AnnotationCategories.hs
Normal file
@@ -0,0 +1,151 @@
|
||||
module Web.View.TypeRegistries.AnnotationCategories where
|
||||
|
||||
import Web.Types
|
||||
import Generated.Types
|
||||
import IHP.Prelude
|
||||
import IHP.ViewPrelude
|
||||
|
||||
data AnnotationCategoriesView = AnnotationCategoriesView { entries :: ![AnnotationCategoryRegistry], hubs :: ![Hub] }
|
||||
data ShowAnnotationCategoryView = ShowAnnotationCategoryView { entry :: !AnnotationCategoryRegistry, mOwner :: !(Maybe Hub) }
|
||||
data NewAnnotationCategoryView = NewAnnotationCategoryView { entry :: !AnnotationCategoryRegistry, hubs :: ![Hub] }
|
||||
data EditAnnotationCategoryView = EditAnnotationCategoryView { entry :: !AnnotationCategoryRegistry, hubs :: ![Hub] }
|
||||
|
||||
hubName :: [Hub] -> Maybe (Id Hub) -> Text
|
||||
hubName _ Nothing = "Framework"
|
||||
hubName hubs (Just i) = maybe "Unknown" (.name) (find (\h -> h.id == i) hubs)
|
||||
|
||||
statusBadge :: Text -> Html
|
||||
statusBadge "active" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-green-100 text-green-800">active</span>|]
|
||||
statusBadge "deprecated" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-amber-100 text-amber-800">deprecated</span>|]
|
||||
statusBadge s = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-600">{s}</span>|]
|
||||
|
||||
instance View AnnotationCategoriesView where
|
||||
html AnnotationCategoriesView { .. } = [hsx|
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold">Annotation Category Registry</h1>
|
||||
<p class="text-sm text-gray-500 mt-1">Framework and domain-owned annotation categories</p>
|
||||
</div>
|
||||
<a href={NewAnnotationCategoryAction}
|
||||
class="bg-indigo-600 text-white text-sm font-medium px-4 py-2 rounded hover:bg-indigo-700">
|
||||
Register Category
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex gap-4 mb-4 text-sm">
|
||||
<a href={WidgetTypeRegistryAction} class="text-gray-500 hover:text-gray-700 pb-1">Widget Types</a>
|
||||
<a href={EventTypeRegistryAction} class="text-gray-500 hover:text-gray-700 pb-1">Event Types</a>
|
||||
<a href={AnnotationCategoryRegistryAction} class="text-indigo-600 font-medium border-b-2 border-indigo-600 pb-1">Annotation Categories</a>
|
||||
<a href={PolicyScopeRegistryAction} class="text-gray-500 hover:text-gray-700 pb-1">Policy Scopes</a>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50 border-b border-gray-200">
|
||||
<tr>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-700">Name</th>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-700">Label</th>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-700">Owner</th>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-700">Status</th>
|
||||
<th class="px-4 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{forEach entries (renderRow hubs)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|]
|
||||
|
||||
renderRow :: [Hub] -> AnnotationCategoryRegistry -> Html
|
||||
renderRow hubs e = [hsx|
|
||||
<tr class="border-b border-gray-100 hover:bg-gray-50">
|
||||
<td class="px-4 py-3 font-mono text-xs">{e.name}</td>
|
||||
<td class="px-4 py-3">{e.label}</td>
|
||||
<td class="px-4 py-3 text-gray-500">{hubName hubs e.ownerHubId}</td>
|
||||
<td class="px-4 py-3">{statusBadge e.status}</td>
|
||||
<td class="px-4 py-3 text-right text-xs">
|
||||
<a href={ShowAnnotationCategoryAction { annotationCategoryRegistryId = e.id }} class="text-gray-500 hover:text-gray-700 mr-2">View</a>
|
||||
<a href={EditAnnotationCategoryAction { annotationCategoryRegistryId = e.id }} class="text-gray-500 hover:text-gray-700">Edit</a>
|
||||
</td>
|
||||
</tr>
|
||||
|]
|
||||
|
||||
instance View ShowAnnotationCategoryView where
|
||||
html ShowAnnotationCategoryView { .. } = [hsx|
|
||||
<div class="mb-4">
|
||||
<a href={AnnotationCategoryRegistryAction} class="text-sm text-gray-500 hover:text-gray-700">← Annotation Categories</a>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6 max-w-lg">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h1 class="text-xl font-semibold">{entry.name}</h1>
|
||||
{statusBadge entry.status}
|
||||
</div>
|
||||
<dl class="space-y-3 text-sm">
|
||||
<div><dt class="text-gray-500">Label</dt><dd class="font-medium">{entry.label}</dd></div>
|
||||
<div><dt class="text-gray-500">Description</dt><dd>{fromMaybe "—" entry.description}</dd></div>
|
||||
<div><dt class="text-gray-500">Owner</dt><dd>{maybe "Framework (cross-domain)" (.name) mOwner}</dd></div>
|
||||
<div><dt class="text-gray-500">Replaced by</dt><dd>{fromMaybe "—" entry.deprecatedInFavourOf}</dd></div>
|
||||
</dl>
|
||||
<div class="mt-6">
|
||||
<a href={EditAnnotationCategoryAction { annotationCategoryRegistryId = entry.id }}
|
||||
class="text-sm border border-gray-300 px-3 py-1.5 rounded hover:bg-gray-50">Edit</a>
|
||||
</div>
|
||||
</div>
|
||||
|]
|
||||
|
||||
typeForm :: AnnotationCategoryRegistry -> [Hub] -> Bool -> Html
|
||||
typeForm entry hubs isNew = [hsx|
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6 max-w-lg">
|
||||
<div class="space-y-4">
|
||||
{if isNew then [hsx|
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Name <span class="text-gray-400 text-xs">(permanent, lowercase-underscored)</span></label>
|
||||
{(textField #name) { fieldClass = "w-full border border-gray-300 rounded px-3 py-2 text-sm" }}
|
||||
</div>
|
||||
|] else [hsx|
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Name <span class="text-gray-400 text-xs">(immutable)</span></label>
|
||||
<p class="font-mono text-sm bg-gray-50 border border-gray-200 rounded px-3 py-2">{entry.name}</p>
|
||||
</div>
|
||||
|]}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Label</label>
|
||||
{(textField #label) { fieldClass = "w-full border border-gray-300 rounded px-3 py-2 text-sm" }}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Description <span class="text-gray-400 text-xs">(optional)</span></label>
|
||||
{(textField #description) { fieldClass = "w-full border border-gray-300 rounded px-3 py-2 text-sm" }}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Owner Hub <span class="text-gray-400 text-xs">(blank = framework-level)</span></label>
|
||||
{selectField #ownerHubId hubs}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<button type="submit" class="bg-indigo-600 text-white text-sm px-4 py-2 rounded hover:bg-indigo-700">
|
||||
{if isNew then ("Register" :: Text) else "Save"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|]
|
||||
|
||||
instance View NewAnnotationCategoryView where
|
||||
html NewAnnotationCategoryView { .. } = [hsx|
|
||||
<div class="mb-4">
|
||||
<a href={AnnotationCategoryRegistryAction} class="text-sm text-gray-500 hover:text-gray-700">← Annotation Categories</a>
|
||||
</div>
|
||||
<h1 class="text-xl font-semibold mb-6">Register Annotation Category</h1>
|
||||
<form method="POST" action={CreateAnnotationCategoryAction}>
|
||||
{typeForm entry hubs True}
|
||||
</form>
|
||||
|]
|
||||
|
||||
instance View EditAnnotationCategoryView where
|
||||
html EditAnnotationCategoryView { .. } = [hsx|
|
||||
<div class="mb-4">
|
||||
<a href={AnnotationCategoryRegistryAction} class="text-sm text-gray-500 hover:text-gray-700">← Annotation Categories</a>
|
||||
</div>
|
||||
<h1 class="text-xl font-semibold mb-6">Edit Annotation Category</h1>
|
||||
<form method="POST" action={UpdateAnnotationCategoryAction { annotationCategoryRegistryId = entry.id }}>
|
||||
{typeForm entry hubs False}
|
||||
</form>
|
||||
|]
|
||||
151
Web/View/TypeRegistries/EventTypes.hs
Normal file
151
Web/View/TypeRegistries/EventTypes.hs
Normal file
@@ -0,0 +1,151 @@
|
||||
module Web.View.TypeRegistries.EventTypes where
|
||||
|
||||
import Web.Types
|
||||
import Generated.Types
|
||||
import IHP.Prelude
|
||||
import IHP.ViewPrelude
|
||||
|
||||
data EventTypesView = EventTypesView { entries :: ![EventTypeRegistry], hubs :: ![Hub] }
|
||||
data ShowEventTypeView = ShowEventTypeView { entry :: !EventTypeRegistry, mOwner :: !(Maybe Hub) }
|
||||
data NewEventTypeView = NewEventTypeView { entry :: !EventTypeRegistry, hubs :: ![Hub] }
|
||||
data EditEventTypeView = EditEventTypeView { entry :: !EventTypeRegistry, hubs :: ![Hub] }
|
||||
|
||||
hubName :: [Hub] -> Maybe (Id Hub) -> Text
|
||||
hubName _ Nothing = "Framework"
|
||||
hubName hubs (Just i) = maybe "Unknown" (.name) (find (\h -> h.id == i) hubs)
|
||||
|
||||
statusBadge :: Text -> Html
|
||||
statusBadge "active" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-green-100 text-green-800">active</span>|]
|
||||
statusBadge "deprecated" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-amber-100 text-amber-800">deprecated</span>|]
|
||||
statusBadge s = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-600">{s}</span>|]
|
||||
|
||||
instance View EventTypesView where
|
||||
html EventTypesView { .. } = [hsx|
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold">Event Type Registry</h1>
|
||||
<p class="text-sm text-gray-500 mt-1">Framework and domain-owned interaction event types</p>
|
||||
</div>
|
||||
<a href={NewEventTypeAction}
|
||||
class="bg-indigo-600 text-white text-sm font-medium px-4 py-2 rounded hover:bg-indigo-700">
|
||||
Register Type
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex gap-4 mb-4 text-sm">
|
||||
<a href={WidgetTypeRegistryAction} class="text-gray-500 hover:text-gray-700 pb-1">Widget Types</a>
|
||||
<a href={EventTypeRegistryAction} class="text-indigo-600 font-medium border-b-2 border-indigo-600 pb-1">Event Types</a>
|
||||
<a href={AnnotationCategoryRegistryAction} class="text-gray-500 hover:text-gray-700 pb-1">Annotation Categories</a>
|
||||
<a href={PolicyScopeRegistryAction} class="text-gray-500 hover:text-gray-700 pb-1">Policy Scopes</a>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50 border-b border-gray-200">
|
||||
<tr>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-700">Name</th>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-700">Label</th>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-700">Owner</th>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-700">Status</th>
|
||||
<th class="px-4 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{forEach entries (renderRow hubs)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|]
|
||||
|
||||
renderRow :: [Hub] -> EventTypeRegistry -> Html
|
||||
renderRow hubs e = [hsx|
|
||||
<tr class="border-b border-gray-100 hover:bg-gray-50">
|
||||
<td class="px-4 py-3 font-mono text-xs">{e.name}</td>
|
||||
<td class="px-4 py-3">{e.label}</td>
|
||||
<td class="px-4 py-3 text-gray-500">{hubName hubs e.ownerHubId}</td>
|
||||
<td class="px-4 py-3">{statusBadge e.status}</td>
|
||||
<td class="px-4 py-3 text-right text-xs">
|
||||
<a href={ShowEventTypeAction { eventTypeRegistryId = e.id }} class="text-gray-500 hover:text-gray-700 mr-2">View</a>
|
||||
<a href={EditEventTypeAction { eventTypeRegistryId = e.id }} class="text-gray-500 hover:text-gray-700">Edit</a>
|
||||
</td>
|
||||
</tr>
|
||||
|]
|
||||
|
||||
instance View ShowEventTypeView where
|
||||
html ShowEventTypeView { .. } = [hsx|
|
||||
<div class="mb-4">
|
||||
<a href={EventTypeRegistryAction} class="text-sm text-gray-500 hover:text-gray-700">← Event Types</a>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6 max-w-lg">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h1 class="text-xl font-semibold">{entry.name}</h1>
|
||||
{statusBadge entry.status}
|
||||
</div>
|
||||
<dl class="space-y-3 text-sm">
|
||||
<div><dt class="text-gray-500">Label</dt><dd class="font-medium">{entry.label}</dd></div>
|
||||
<div><dt class="text-gray-500">Description</dt><dd>{fromMaybe "—" entry.description}</dd></div>
|
||||
<div><dt class="text-gray-500">Owner</dt><dd>{maybe "Framework (cross-domain)" (.name) mOwner}</dd></div>
|
||||
<div><dt class="text-gray-500">Replaced by</dt><dd>{fromMaybe "—" entry.deprecatedInFavourOf}</dd></div>
|
||||
</dl>
|
||||
<div class="mt-6 flex gap-2">
|
||||
<a href={EditEventTypeAction { eventTypeRegistryId = entry.id }}
|
||||
class="text-sm border border-gray-300 px-3 py-1.5 rounded hover:bg-gray-50">Edit</a>
|
||||
</div>
|
||||
</div>
|
||||
|]
|
||||
|
||||
typeForm :: EventTypeRegistry -> [Hub] -> Bool -> Html
|
||||
typeForm entry hubs isNew = [hsx|
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6 max-w-lg">
|
||||
<div class="space-y-4">
|
||||
{if isNew then [hsx|
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Name <span class="text-gray-400 text-xs">(permanent, lowercase-underscored)</span></label>
|
||||
{(textField #name) { fieldClass = "w-full border border-gray-300 rounded px-3 py-2 text-sm" }}
|
||||
</div>
|
||||
|] else [hsx|
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Name <span class="text-gray-400 text-xs">(immutable)</span></label>
|
||||
<p class="font-mono text-sm bg-gray-50 border border-gray-200 rounded px-3 py-2">{entry.name}</p>
|
||||
</div>
|
||||
|]}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Label</label>
|
||||
{(textField #label) { fieldClass = "w-full border border-gray-300 rounded px-3 py-2 text-sm" }}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Description <span class="text-gray-400 text-xs">(optional)</span></label>
|
||||
{(textField #description) { fieldClass = "w-full border border-gray-300 rounded px-3 py-2 text-sm" }}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Owner Hub <span class="text-gray-400 text-xs">(blank = framework-level)</span></label>
|
||||
{selectField #ownerHubId hubs}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<button type="submit" class="bg-indigo-600 text-white text-sm px-4 py-2 rounded hover:bg-indigo-700">
|
||||
{if isNew then ("Register" :: Text) else "Save"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|]
|
||||
|
||||
instance View NewEventTypeView where
|
||||
html NewEventTypeView { .. } = [hsx|
|
||||
<div class="mb-4">
|
||||
<a href={EventTypeRegistryAction} class="text-sm text-gray-500 hover:text-gray-700">← Event Types</a>
|
||||
</div>
|
||||
<h1 class="text-xl font-semibold mb-6">Register Event Type</h1>
|
||||
<form method="POST" action={CreateEventTypeAction}>
|
||||
{typeForm entry hubs True}
|
||||
</form>
|
||||
|]
|
||||
|
||||
instance View EditEventTypeView where
|
||||
html EditEventTypeView { .. } = [hsx|
|
||||
<div class="mb-4">
|
||||
<a href={EventTypeRegistryAction} class="text-sm text-gray-500 hover:text-gray-700">← Event Types</a>
|
||||
</div>
|
||||
<h1 class="text-xl font-semibold mb-6">Edit Event Type</h1>
|
||||
<form method="POST" action={UpdateEventTypeAction { eventTypeRegistryId = entry.id }}>
|
||||
{typeForm entry hubs False}
|
||||
</form>
|
||||
|]
|
||||
151
Web/View/TypeRegistries/PolicyScopes.hs
Normal file
151
Web/View/TypeRegistries/PolicyScopes.hs
Normal file
@@ -0,0 +1,151 @@
|
||||
module Web.View.TypeRegistries.PolicyScopes where
|
||||
|
||||
import Web.Types
|
||||
import Generated.Types
|
||||
import IHP.Prelude
|
||||
import IHP.ViewPrelude
|
||||
|
||||
data PolicyScopesView = PolicyScopesView { entries :: ![PolicyScopeRegistry], hubs :: ![Hub] }
|
||||
data ShowPolicyScopeView = ShowPolicyScopeView { entry :: !PolicyScopeRegistry, mOwner :: !(Maybe Hub) }
|
||||
data NewPolicyScopeView = NewPolicyScopeView { entry :: !PolicyScopeRegistry, hubs :: ![Hub] }
|
||||
data EditPolicyScopeView = EditPolicyScopeView { entry :: !PolicyScopeRegistry, hubs :: ![Hub] }
|
||||
|
||||
hubName :: [Hub] -> Maybe (Id Hub) -> Text
|
||||
hubName _ Nothing = "Framework"
|
||||
hubName hubs (Just i) = maybe "Unknown" (.name) (find (\h -> h.id == i) hubs)
|
||||
|
||||
statusBadge :: Text -> Html
|
||||
statusBadge "active" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-green-100 text-green-800">active</span>|]
|
||||
statusBadge "deprecated" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-amber-100 text-amber-800">deprecated</span>|]
|
||||
statusBadge s = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-600">{s}</span>|]
|
||||
|
||||
instance View PolicyScopesView where
|
||||
html PolicyScopesView { .. } = [hsx|
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold">Policy Scope Registry</h1>
|
||||
<p class="text-sm text-gray-500 mt-1">Framework and domain-owned policy scopes</p>
|
||||
</div>
|
||||
<a href={NewPolicyScopeAction}
|
||||
class="bg-indigo-600 text-white text-sm font-medium px-4 py-2 rounded hover:bg-indigo-700">
|
||||
Register Scope
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex gap-4 mb-4 text-sm">
|
||||
<a href={WidgetTypeRegistryAction} class="text-gray-500 hover:text-gray-700 pb-1">Widget Types</a>
|
||||
<a href={EventTypeRegistryAction} class="text-gray-500 hover:text-gray-700 pb-1">Event Types</a>
|
||||
<a href={AnnotationCategoryRegistryAction} class="text-gray-500 hover:text-gray-700 pb-1">Annotation Categories</a>
|
||||
<a href={PolicyScopeRegistryAction} class="text-indigo-600 font-medium border-b-2 border-indigo-600 pb-1">Policy Scopes</a>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50 border-b border-gray-200">
|
||||
<tr>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-700">Name</th>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-700">Label</th>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-700">Owner</th>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-700">Status</th>
|
||||
<th class="px-4 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{forEach entries (renderRow hubs)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|]
|
||||
|
||||
renderRow :: [Hub] -> PolicyScopeRegistry -> Html
|
||||
renderRow hubs e = [hsx|
|
||||
<tr class="border-b border-gray-100 hover:bg-gray-50">
|
||||
<td class="px-4 py-3 font-mono text-xs">{e.name}</td>
|
||||
<td class="px-4 py-3">{e.label}</td>
|
||||
<td class="px-4 py-3 text-gray-500">{hubName hubs e.ownerHubId}</td>
|
||||
<td class="px-4 py-3">{statusBadge e.status}</td>
|
||||
<td class="px-4 py-3 text-right text-xs">
|
||||
<a href={ShowPolicyScopeAction { policyScopeRegistryId = e.id }} class="text-gray-500 hover:text-gray-700 mr-2">View</a>
|
||||
<a href={EditPolicyScopeAction { policyScopeRegistryId = e.id }} class="text-gray-500 hover:text-gray-700">Edit</a>
|
||||
</td>
|
||||
</tr>
|
||||
|]
|
||||
|
||||
instance View ShowPolicyScopeView where
|
||||
html ShowPolicyScopeView { .. } = [hsx|
|
||||
<div class="mb-4">
|
||||
<a href={PolicyScopeRegistryAction} class="text-sm text-gray-500 hover:text-gray-700">← Policy Scopes</a>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6 max-w-lg">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h1 class="text-xl font-semibold">{entry.name}</h1>
|
||||
{statusBadge entry.status}
|
||||
</div>
|
||||
<dl class="space-y-3 text-sm">
|
||||
<div><dt class="text-gray-500">Label</dt><dd class="font-medium">{entry.label}</dd></div>
|
||||
<div><dt class="text-gray-500">Description</dt><dd>{fromMaybe "—" entry.description}</dd></div>
|
||||
<div><dt class="text-gray-500">Owner</dt><dd>{maybe "Framework (cross-domain)" (.name) mOwner}</dd></div>
|
||||
<div><dt class="text-gray-500">Replaced by</dt><dd>{fromMaybe "—" entry.deprecatedInFavourOf}</dd></div>
|
||||
</dl>
|
||||
<div class="mt-6">
|
||||
<a href={EditPolicyScopeAction { policyScopeRegistryId = entry.id }}
|
||||
class="text-sm border border-gray-300 px-3 py-1.5 rounded hover:bg-gray-50">Edit</a>
|
||||
</div>
|
||||
</div>
|
||||
|]
|
||||
|
||||
typeForm :: PolicyScopeRegistry -> [Hub] -> Bool -> Html
|
||||
typeForm entry hubs isNew = [hsx|
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6 max-w-lg">
|
||||
<div class="space-y-4">
|
||||
{if isNew then [hsx|
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Name <span class="text-gray-400 text-xs">(permanent, lowercase-hyphenated)</span></label>
|
||||
{(textField #name) { fieldClass = "w-full border border-gray-300 rounded px-3 py-2 text-sm" }}
|
||||
</div>
|
||||
|] else [hsx|
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Name <span class="text-gray-400 text-xs">(immutable)</span></label>
|
||||
<p class="font-mono text-sm bg-gray-50 border border-gray-200 rounded px-3 py-2">{entry.name}</p>
|
||||
</div>
|
||||
|]}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Label</label>
|
||||
{(textField #label) { fieldClass = "w-full border border-gray-300 rounded px-3 py-2 text-sm" }}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Description <span class="text-gray-400 text-xs">(optional)</span></label>
|
||||
{(textField #description) { fieldClass = "w-full border border-gray-300 rounded px-3 py-2 text-sm" }}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Owner Hub <span class="text-gray-400 text-xs">(blank = framework-level)</span></label>
|
||||
{selectField #ownerHubId hubs}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<button type="submit" class="bg-indigo-600 text-white text-sm px-4 py-2 rounded hover:bg-indigo-700">
|
||||
{if isNew then ("Register" :: Text) else "Save"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|]
|
||||
|
||||
instance View NewPolicyScopeView where
|
||||
html NewPolicyScopeView { .. } = [hsx|
|
||||
<div class="mb-4">
|
||||
<a href={PolicyScopeRegistryAction} class="text-sm text-gray-500 hover:text-gray-700">← Policy Scopes</a>
|
||||
</div>
|
||||
<h1 class="text-xl font-semibold mb-6">Register Policy Scope</h1>
|
||||
<form method="POST" action={CreatePolicyScopeAction}>
|
||||
{typeForm entry hubs True}
|
||||
</form>
|
||||
|]
|
||||
|
||||
instance View EditPolicyScopeView where
|
||||
html EditPolicyScopeView { .. } = [hsx|
|
||||
<div class="mb-4">
|
||||
<a href={PolicyScopeRegistryAction} class="text-sm text-gray-500 hover:text-gray-700">← Policy Scopes</a>
|
||||
</div>
|
||||
<h1 class="text-xl font-semibold mb-6">Edit Policy Scope</h1>
|
||||
<form method="POST" action={UpdatePolicyScopeAction { policyScopeRegistryId = entry.id }}>
|
||||
{typeForm entry hubs False}
|
||||
</form>
|
||||
|]
|
||||
159
Web/View/TypeRegistries/WidgetTypes.hs
Normal file
159
Web/View/TypeRegistries/WidgetTypes.hs
Normal file
@@ -0,0 +1,159 @@
|
||||
module Web.View.TypeRegistries.WidgetTypes where
|
||||
|
||||
import Web.Types
|
||||
import Generated.Types
|
||||
import IHP.Prelude
|
||||
import IHP.ViewPrelude
|
||||
|
||||
data WidgetTypesView = WidgetTypesView { entries :: ![WidgetTypeRegistry], hubs :: ![Hub] }
|
||||
data ShowWidgetTypeView = ShowWidgetTypeView { entry :: !WidgetTypeRegistry, mOwner :: !(Maybe Hub) }
|
||||
data NewWidgetTypeView = NewWidgetTypeView { entry :: !WidgetTypeRegistry, hubs :: ![Hub] }
|
||||
data EditWidgetTypeView = EditWidgetTypeView { entry :: !WidgetTypeRegistry, hubs :: ![Hub] }
|
||||
|
||||
hubName :: [Hub] -> Maybe (Id Hub) -> Text
|
||||
hubName _ Nothing = "Framework"
|
||||
hubName hubs (Just i) = maybe "Unknown" (.name) (find (\h -> h.id == i) hubs)
|
||||
|
||||
statusBadge :: Text -> Html
|
||||
statusBadge "active" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-green-100 text-green-800">active</span>|]
|
||||
statusBadge "deprecated" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-amber-100 text-amber-800">deprecated</span>|]
|
||||
statusBadge s = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-600">{s}</span>|]
|
||||
|
||||
instance View WidgetTypesView where
|
||||
html WidgetTypesView { .. } = [hsx|
|
||||
<div class="flex items-center justify-between mb-6">
|
||||
<div>
|
||||
<h1 class="text-2xl font-semibold">Widget Type Registry</h1>
|
||||
<p class="text-sm text-gray-500 mt-1">Framework and domain-owned widget types</p>
|
||||
</div>
|
||||
<a href={NewWidgetTypeAction}
|
||||
class="bg-indigo-600 text-white text-sm font-medium px-4 py-2 rounded hover:bg-indigo-700">
|
||||
Register Type
|
||||
</a>
|
||||
</div>
|
||||
<div class="flex gap-4 mb-4 text-sm">
|
||||
<a href={WidgetTypeRegistryAction} class="text-indigo-600 font-medium border-b-2 border-indigo-600 pb-1">Widget Types</a>
|
||||
<a href={EventTypeRegistryAction} class="text-gray-500 hover:text-gray-700 pb-1">Event Types</a>
|
||||
<a href={AnnotationCategoryRegistryAction} class="text-gray-500 hover:text-gray-700 pb-1">Annotation Categories</a>
|
||||
<a href={PolicyScopeRegistryAction} class="text-gray-500 hover:text-gray-700 pb-1">Policy Scopes</a>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg border border-gray-200 overflow-hidden">
|
||||
<table class="w-full text-sm">
|
||||
<thead class="bg-gray-50 border-b border-gray-200">
|
||||
<tr>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-700">Name</th>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-700">Label</th>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-700">Owner</th>
|
||||
<th class="text-left px-4 py-3 font-medium text-gray-700">Status</th>
|
||||
<th class="px-4 py-3"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{forEach entries (renderRow hubs)}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
|]
|
||||
|
||||
renderRow :: [Hub] -> WidgetTypeRegistry -> Html
|
||||
renderRow hubs e = [hsx|
|
||||
<tr class="border-b border-gray-100 hover:bg-gray-50">
|
||||
<td class="px-4 py-3 font-mono text-xs">{e.name}</td>
|
||||
<td class="px-4 py-3">{e.label}</td>
|
||||
<td class="px-4 py-3 text-gray-500">{hubName hubs e.ownerHubId}</td>
|
||||
<td class="px-4 py-3">{statusBadge e.status}</td>
|
||||
<td class="px-4 py-3 text-right text-xs">
|
||||
<a href={ShowWidgetTypeAction { widgetTypeRegistryId = e.id }} class="text-gray-500 hover:text-gray-700 mr-2">View</a>
|
||||
<a href={EditWidgetTypeAction { widgetTypeRegistryId = e.id }} class="text-gray-500 hover:text-gray-700">Edit</a>
|
||||
</td>
|
||||
</tr>
|
||||
|]
|
||||
|
||||
instance View ShowWidgetTypeView where
|
||||
html ShowWidgetTypeView { .. } = [hsx|
|
||||
<div class="mb-4">
|
||||
<a href={WidgetTypeRegistryAction} class="text-sm text-gray-500 hover:text-gray-700">← Widget Types</a>
|
||||
</div>
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6 max-w-lg">
|
||||
<div class="flex items-center justify-between mb-4">
|
||||
<h1 class="text-xl font-semibold">{entry.name}</h1>
|
||||
{statusBadge entry.status}
|
||||
</div>
|
||||
<dl class="space-y-3 text-sm">
|
||||
<div><dt class="text-gray-500">Label</dt><dd class="font-medium">{entry.label}</dd></div>
|
||||
<div><dt class="text-gray-500">Description</dt><dd>{fromMaybe "—" entry.description}</dd></div>
|
||||
<div><dt class="text-gray-500">Owner</dt><dd>{maybe "Framework (cross-domain)" (.name) mOwner}</dd></div>
|
||||
<div><dt class="text-gray-500">Replaced by</dt><dd>{fromMaybe "—" entry.deprecatedInFavourOf}</dd></div>
|
||||
</dl>
|
||||
<div class="mt-6 flex gap-2">
|
||||
<a href={EditWidgetTypeAction { widgetTypeRegistryId = entry.id }}
|
||||
class="text-sm border border-gray-300 px-3 py-1.5 rounded hover:bg-gray-50">Edit</a>
|
||||
{if entry.status == "active"
|
||||
then [hsx|
|
||||
<form method="POST" action={DeprecateWidgetTypeAction { widgetTypeRegistryId = entry.id }}>
|
||||
<input type="hidden" name="deprecated_in_favour_of" value="" placeholder="replacement name" />
|
||||
<button type="submit" class="text-sm border border-amber-300 text-amber-700 px-3 py-1.5 rounded hover:bg-amber-50">Deprecate</button>
|
||||
</form>
|
||||
|]
|
||||
else mempty}
|
||||
</div>
|
||||
</div>
|
||||
|]
|
||||
|
||||
typeForm :: WidgetTypeRegistry -> [Hub] -> Bool -> Html
|
||||
typeForm entry hubs isNew = [hsx|
|
||||
<div class="bg-white rounded-lg border border-gray-200 p-6 max-w-lg">
|
||||
<div class="space-y-4">
|
||||
{if isNew then [hsx|
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Name <span class="text-gray-400 text-xs">(permanent identifier, lowercase-hyphenated)</span></label>
|
||||
{(textField #name) { fieldClass = "w-full border border-gray-300 rounded px-3 py-2 text-sm" }}
|
||||
</div>
|
||||
|] else [hsx|
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Name <span class="text-gray-400 text-xs">(immutable)</span></label>
|
||||
<p class="font-mono text-sm bg-gray-50 border border-gray-200 rounded px-3 py-2">{entry.name}</p>
|
||||
</div>
|
||||
|]}
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Label</label>
|
||||
{(textField #label) { fieldClass = "w-full border border-gray-300 rounded px-3 py-2 text-sm" }}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Description <span class="text-gray-400 text-xs">(optional)</span></label>
|
||||
{(textField #description) { fieldClass = "w-full border border-gray-300 rounded px-3 py-2 text-sm" }}
|
||||
</div>
|
||||
<div>
|
||||
<label class="block text-sm font-medium text-gray-700 mb-1">Owner Hub <span class="text-gray-400 text-xs">(leave blank for framework-level)</span></label>
|
||||
{selectField #ownerHubId hubs}
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-6">
|
||||
<button type="submit" class="bg-indigo-600 text-white text-sm px-4 py-2 rounded hover:bg-indigo-700">
|
||||
{if isNew then ("Register" :: Text) else "Save"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|]
|
||||
|
||||
instance View NewWidgetTypeView where
|
||||
html NewWidgetTypeView { .. } = [hsx|
|
||||
<div class="mb-4">
|
||||
<a href={WidgetTypeRegistryAction} class="text-sm text-gray-500 hover:text-gray-700">← Widget Types</a>
|
||||
</div>
|
||||
<h1 class="text-xl font-semibold mb-6">Register Widget Type</h1>
|
||||
<form method="POST" action={CreateWidgetTypeAction}>
|
||||
{typeForm entry hubs True}
|
||||
</form>
|
||||
|]
|
||||
|
||||
instance View EditWidgetTypeView where
|
||||
html EditWidgetTypeView { .. } = [hsx|
|
||||
<div class="mb-4">
|
||||
<a href={WidgetTypeRegistryAction} class="text-sm text-gray-500 hover:text-gray-700">← Widget Types</a>
|
||||
</div>
|
||||
<h1 class="text-xl font-semibold mb-6">Edit Widget Type</h1>
|
||||
<form method="POST" action={UpdateWidgetTypeAction { widgetTypeRegistryId = entry.id }}>
|
||||
{typeForm entry hubs False}
|
||||
</form>
|
||||
|]
|
||||
@@ -30,6 +30,7 @@ instance View ShowView where
|
||||
<span class={adapterStatusBadge spec.status <> " text-xs px-2 py-0.5 rounded font-medium"}>
|
||||
{spec.status}
|
||||
</span>
|
||||
{maturityBadge spec.maturity}
|
||||
</div>
|
||||
<a href={EditWidgetAdapterSpecAction { widgetAdapterSpecId = spec.id }}
|
||||
class="text-sm text-gray-500 border border-gray-200 rounded px-3 py-1 hover:border-gray-400">
|
||||
@@ -116,3 +117,10 @@ specNotes :: WidgetAdapterSpec -> [Text]
|
||||
specNotes s = case s.notes of
|
||||
Just n -> [n]
|
||||
Nothing -> []
|
||||
|
||||
maturityBadge :: Text -> Html
|
||||
maturityBadge "stable" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-green-100 text-green-800 font-medium">Stable</span>|]
|
||||
maturityBadge "beta" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-blue-100 text-blue-800 font-medium">Beta</span>|]
|
||||
maturityBadge "experimental" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-amber-100 text-amber-800 font-medium">Experimental</span>|]
|
||||
maturityBadge "deprecated" = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-500 font-medium">Deprecated</span>|]
|
||||
maturityBadge m = [hsx|<span class="px-2 py-0.5 rounded text-xs bg-gray-100 text-gray-600 font-medium">{m}</span>|]
|
||||
|
||||
@@ -10,6 +10,8 @@ data EditView = EditView
|
||||
{ widget :: !Widget
|
||||
, hubs :: ![Hub]
|
||||
, adapterSpecs :: ![WidgetAdapterSpec]
|
||||
, widgetTypes :: ![WidgetTypeRegistry]
|
||||
, policyScopes :: ![PolicyScopeRegistry]
|
||||
}
|
||||
|
||||
instance View EditView where
|
||||
@@ -23,6 +25,6 @@ instance View EditView where
|
||||
<span>Edit</span>
|
||||
</div>
|
||||
<h1 class="text-2xl font-semibold mb-6">Edit Widget</h1>
|
||||
{renderForm widget hubs adapterSpecs}
|
||||
{renderForm widget hubs adapterSpecs widgetTypes policyScopes}
|
||||
</div>
|
||||
|]
|
||||
|
||||
@@ -9,24 +9,26 @@ data NewView = NewView
|
||||
{ widget :: !Widget
|
||||
, hubs :: ![Hub]
|
||||
, adapterSpecs :: ![WidgetAdapterSpec]
|
||||
, widgetTypes :: ![WidgetTypeRegistry]
|
||||
, policyScopes :: ![PolicyScopeRegistry]
|
||||
}
|
||||
|
||||
instance View NewView where
|
||||
html NewView { .. } = [hsx|
|
||||
<div class="max-w-lg">
|
||||
<h1 class="text-2xl font-semibold mb-6">Register Widget</h1>
|
||||
{renderForm widget hubs adapterSpecs}
|
||||
{renderForm widget hubs adapterSpecs widgetTypes policyScopes}
|
||||
</div>
|
||||
|]
|
||||
|
||||
renderForm :: Widget -> [Hub] -> [WidgetAdapterSpec] -> Html
|
||||
renderForm widget hubs adapterSpecs = formFor widget [hsx|
|
||||
renderForm :: Widget -> [Hub] -> [WidgetAdapterSpec] -> [WidgetTypeRegistry] -> [PolicyScopeRegistry] -> Html
|
||||
renderForm widget hubs adapterSpecs widgetTypes policyScopes = formFor widget [hsx|
|
||||
{textField #name}
|
||||
{selectField #widgetType widgetTypeOptions}
|
||||
{selectField #widgetType (widgetTypeOptions widgetTypes)}
|
||||
{selectField #hubId (hubOptions hubs)}
|
||||
{textField #capabilityRef}
|
||||
{textField #viewContext}
|
||||
{selectField #policyScope policyScopeOptions}
|
||||
{selectField #policyScope (policyScopeOptions policyScopes)}
|
||||
{selectField #status statusOptions}
|
||||
<div>
|
||||
<label class="ihp-form-label">Adapter Spec (optional — leave blank for native IHP widget)</label>
|
||||
@@ -43,23 +45,11 @@ renderForm widget hubs adapterSpecs = formFor widget [hsx|
|
||||
hubOptions :: [Hub] -> [(Text, Id Hub)]
|
||||
hubOptions hubs = map (\h -> (h.name, h.id)) hubs
|
||||
|
||||
widgetTypeOptions :: [(Text, Text)]
|
||||
widgetTypeOptions =
|
||||
[ ("Chart", "chart")
|
||||
, ("Form", "form")
|
||||
, ("Table", "table")
|
||||
, ("Action", "action")
|
||||
, ("Panel", "panel")
|
||||
, ("Navigation", "nav")
|
||||
, ("Other", "other")
|
||||
]
|
||||
widgetTypeOptions :: [WidgetTypeRegistry] -> [(Text, Text)]
|
||||
widgetTypeOptions = map (\r -> (r.label, r.name))
|
||||
|
||||
policyScopeOptions :: [(Text, Text)]
|
||||
policyScopeOptions =
|
||||
[ ("Internal", "internal")
|
||||
, ("Hub", "hub")
|
||||
, ("Public", "public")
|
||||
]
|
||||
policyScopeOptions :: [PolicyScopeRegistry] -> [(Text, Text)]
|
||||
policyScopeOptions = map (\r -> (r.label, r.name))
|
||||
|
||||
statusOptions :: [(Text, Text)]
|
||||
statusOptions =
|
||||
|
||||
Reference in New Issue
Block a user