ihp-ide-1.4.0: IHP/IDE/SchemaDesigner/View/Layout.hs
module IHP.IDE.SchemaDesigner.View.Layout
( schemaDesignerLayout
, findStatementByName
, visualNav
, renderColumnSelector
, renderColumn
, renderEnumSelector
, renderValue
, renderObjectSelector
, removeQuotes
, replace
, findForeignKey
, findTableIndex
, migrationStatus
, emptyColumnSelectorContainer
) where
import IHP.ViewPrelude hiding (primaryKeyColumnNames)
import IHP.IDE.SchemaDesigner.Types
import IHP.IDE.ToolServer.Types
import IHP.IDE.ToolServer.Helper.View
import IHP.IDE.ToolServer.Layout hiding (tableIcon)
import IHP.IDE.SchemaDesigner.Compiler (compilePostgresType, compileExpression)
import qualified Data.List as List
schemaDesignerLayout :: Html -> Html
schemaDesignerLayout inner = toolServerLayout [hsx|
<div class={classes ["d-flex d-flex flex-column h-100", ("migration-status-visible", hasUnmigratedChanges)]}>
{visualNav}
<div class="h-100 d-flex flex-column">
{inner}
</div>
</div>
|]
where
(DatabaseNeedsMigration hasUnmigratedChanges) = fromFrozenContext @DatabaseNeedsMigration
unmigratedChanges :: Html
unmigratedChanges = [hsx|
<div class="alert alert-primary d-flex align-items-center" role="alert">
<div style="height: fit-content">
<strong>Unmigrated Changes</strong>
Your app database is not in sync with the Schema.sql
</div>
{databaseControls}
</div>
|]
migrationStatus :: Html
migrationStatus = if hasPendingMigrations
then pendingMigrations
else if databaseNeedsMigration
then unmigratedChanges
else mempty
where
(DatabaseNeedsMigration databaseNeedsMigration) = fromFrozenContext @DatabaseNeedsMigration
hasPendingMigrations :: Bool
hasPendingMigrations = False
unmigratedChanges :: Html
unmigratedChanges = [hsx|
<div id="migration-status-container">
<div class="alert alert-primary d-flex align-items-center shadow-lg" role="alert">
{migrationStatusIcon}
<div class="user-select-none">
<div><strong>Unmigrated Changes</strong></div>
Your app database is not in sync with the Schema
</div>
<div class="ml-auto d-flex justify-content-end">
<a
href={NewMigrationAction}
class="btn px-4 btn-dark"
>Migrate DB <span class="btn-arrow">→</span></a>
</div>
</div>
</div>
|]
pendingMigrations :: Html
pendingMigrations = [hsx|
<div id="migration-status-container">
<div class="alert alert-primary d-flex align-items-center shadow-lg" role="alert">
{migrationStatusIcon}
<div class="user-select-none">
<div><strong>Pending Changes</strong></div>
You have migrations that haven't been run yet
</div>
<div class="ml-auto d-flex justify-content-end">
<a
href="#"
class="btn px-4 btn-dark"
>Run Migrations <span class="btn-arrow">→</span></a>
</div>
</div>
</div>
|]
migrationStatusIcon :: Html
migrationStatusIcon = preEscapedToHtml [plain|
<svg width="33px" height="33px" viewBox="0 0 33 33" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" class="mr-3">
<g id="Schema" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
<g id="Message" transform="translate(-383.000000, -813.000000)" fill="#FFCD1B">
<g id="Group" transform="translate(383.000000, 813.000000)">
<rect id="Rectangle" fill-opacity="0.322170017" x="0" y="0" width="33" height="33" rx="5"></rect>
<g id="Arrow-Double-3---24px" transform="translate(7.000000, 7.000000)">
<path d="M4.39179,10.2850058 C4.78668,9.89890559 5.41981,9.9060056 5.80592,10.3009058 C6.19203,10.6958061 6.18492,11.3289064 5.79003,11.7150067 L3.4531,14.0000172 L19,14.0000172 C19.5523,14.0000172 20,14.4477083 20,15.0000172 C20,15.5523089 19.5523,16.0000172 19,16.0000172 L3.4531,16.0000172 L5.79003,18.2850105 C6.18492,18.6711107 6.19203,19.3042111 5.80592,19.6991113 C5.41981,20.0940116 4.78668,20.1011116 4.39179,19.7150113 L0.30088,15.715009 C0.10847,15.5269089 0,15.2691087 0,15.0000172 C0,14.7309084 0.10847,14.4731083 0.30088,14.2850082 L4.39179,10.2850058 Z M15.6082,0.284989971 C15.2133,-0.101120255 14.5802,-0.0940002508 14.1941,0.30087998 C13.808,0.695770211 13.8151,1.32890058 14.21,1.71501081 L16.5469,4 L1,4 C0.44772,4 0,4.44771241 0,5 C0,5.55228305 0.44772,6 1,6 L16.5469,6 L14.21,8.28500465 C13.8151,8.67110488 13.808,9.30420525 14.1941,9.69910548 C14.5802,10.0940057 15.2133,10.1011057 15.6082,9.71500549 L19.6991,5.71501315 C19.8915,5.52687304 20,5.26911289 20,5 C20,4.73089257 19.8915,4.47313242 19.6991,4.28499231 L15.6082,0.284989971 Z" id="Shape"></path>
</g>
</g>
</g>
</g>
</svg>
|]
databaseControls :: Html
databaseControls = [hsx|
<div class="d-flex justify-content-end ml-auto">
<form method="POST" action={pathTo UpdateDbAction} id="update-db-form"/>
<form method="POST" action={pathTo PushToDbAction} id="push-to-db-form"/>
<form method="POST" action={pathTo DumpDbAction} id="db-to-fixtures-form"/>
<div class="btn-group btn-group-sm">
<a
class="btn btn-primary"
href={NewMigrationAction}
onclick="checkBeforeUnload()"
>Migrate DB →</a>
<button type="button" class="btn btn-primary dropdown-toggle dropdown-toggle-split" data-toggle="dropdown" aria-haspopup="true" aria-expanded="false">
<span class="sr-only">Toggle Dropdown</span>
</button>
<div class="dropdown-menu dropdown-menu-right" aria-labelledby="dropdownMenuLink">
<button
type="submit"
form="update-db-form"
class="dropdown-item"
data-toggle="tooltip"
data-placement="left"
data-html="true"
title="Dumps DB to Fixtures.sql.<br><br>Delete the DB.<br><br>Recreate using Schema.sql and Fixtures.sql"
onclick="checkBeforeUnload()"
>Update DB</button>
<button
type="submit"
class="dropdown-item"
form="db-to-fixtures-form"
data-toggle="tooltip"
data-placement="left"
data-html="true"
title="Saves the content of all tables to Application/Fixtures.sql"
onclick="checkBeforeUnload()"
>Save DB to Fixtures</button>
<button
type="submit"
class="dropdown-item"
form="push-to-db-form"
data-toggle="tooltip"
data-placement="left"
data-html="true"
title="Delete the DB and recreate using Application/Schema.sql and Application/Fixture.sql<br><br><strong class=text-danger>Save DB to Fixtures before using this to avoid data loss</strong>"
onclick="checkBeforeUnload()"
>Push to DB</button>
</div>
</div>
</div>
|]
findStatementByName statementName statements = find pred statements
where
pred (StatementCreateTable CreateTable { name }) | (toUpper name) == (toUpper statementName) = True
pred (StatementCreateTable CreateTable { name }) | (toUpper name) == (toUpper (tshow statementName)) = True
pred CreateEnumType { name } | (toUpper name) == (toUpper statementName) = True
pred CreateEnumType { name } | (toUpper name) == (toUpper (tshow statementName)) = True
pred _ = False
visualNav :: Html
visualNav = [hsx|
<div class="view-selector">
<div class="container-fluid">
<a href={TablesAction} class={classes [("active", tableViewActive)]}>
Schema Designer
</a>
<a href={ShowCodeAction} class={classes [("active", codeEditorActive)]}>
Code Editor
</a>
<a href={MigrationsAction} class={classes [("active", migrationsActive)]}>
Migrations
</a>
</div>
</div>
|]
where
codeEditorActive = isActivePath ShowCodeAction
tableViewActive = not codeEditorActive && not migrationsActive
migrationsActive = isActiveController @MigrationsController
emptyColumnSelectorContainer = [hsx|
<div class="col-md-8 col-lg-10 column-selector d-flex">
</div>
|]
renderColumnSelector :: Text -> [(Int, Column)] -> [Statement] -> Html
renderColumnSelector tableName columns statements = [hsx|
<div class="col-md-8 col-lg-10 column-selector d-flex">
<section class="flex-grow-1" oncontextmenu="showContextMenu('context-menu-column-root')">
<div class="d-flex align-items-center">
<h5>Columns</h5>
<div class="toolbox">
<a
href={NewColumnAction tableName}
class="btn btn-link btn-add"
data-toggle="tooltip"
data-placement="bottom"
title="Add Column"
>{addIcon}</a>
</div>
</div>
<table class="table table-hover table-sm">
<tbody>
{forEach columns (\column -> renderColumn (snd column) (fst column) tableName statements)}
</tbody>
</table>
{suggestedColumnsSection tableName columns}
</section>
{auth}
<section>
{columnIndexes}
</section>
</div>
<div class="custom-menu menu-for-column shadow backdrop-blur" id="context-menu-column-root">
<a href={NewColumnAction tableName}>Add Column</a>
<a href={NewPolicyAction tableName}>Add Policy</a>
</div>
|]
where
columnIndexes =
case findTableIndex statements tableName of
Just _ -> [hsx|
<div>
<h5>Indexes</h5>
</div>
<table class="table table-hover table-sm">
{renderColumnIndexes tableName statements}
</table>
|]
Nothing -> [hsx||]
auth :: Html
auth = renderPolicies tableName statements
suggestedColumnsSection :: Text -> [(Int, Column)] -> Html
suggestedColumnsSection tableName indexAndColumns = unless isUsersTable [hsx|
<div class="mt-5">
{mconcat suggestions}
</div>
|]
where
columns :: [Column]
columns = map snd indexAndColumns
hasColumn :: Text -> Bool
hasColumn name = columns |> find (\column -> column.name == name) |> isJust
isUsersTable = tableName == "users"
suggestions = [createdAt, updatedAt, userId] |> catMaybes
createdAt = if hasColumn "created_at"
then Nothing
else Just [hsx|
<form method="POST" action={CreateColumnAction} class="mb-2">
<input type="hidden" name="tableName" value={tableName} />
<input type="hidden" name="name" value="created_at"/>
<input type="hidden" name="columnType" value="TIMESTAMP WITH TIME ZONE"/>
<input type="hidden" name="primaryKey" value={inputValue False}/>
<input type="hidden" name="isArray" value={inputValue False}/>
<input type="hidden" name="defaultValue" value="NOW()"/>
<input type="hidden" name="allowNull" value={inputValue False}/>
<input type="hidden" name="isUnique" value={inputValue False}/>
<input type="hidden" name="isReference" value={inputValue False}/>
<input type="hidden" name="withIndex" value={inputValue True}/>
<button type="submit" class="btn btn-suggested-table">
<table class="table table-sm mb-0">
<tbody>
<tr class="column">
<td style="width: 40px">+</td>
<td class="context-column column-name">created_at</td>
<td class="context-column">TIMESTAMP WITH TIME ZONE</td>
<td class="context-column">NOW()</td>
<td class="context-column"></td>
</tr>
</tbody>
</table>
</button>
</form>
|]
updatedAt = if hasColumn "updated_at"
then Nothing
else Just [hsx|
<form method="POST" action={CreateColumnAction} class="mb-2">
<input type="hidden" name="tableName" value={tableName} />
<input type="hidden" name="name" value="updated_at"/>
<input type="hidden" name="columnType" value="TIMESTAMP WITH TIME ZONE"/>
<input type="hidden" name="primaryKey" value={inputValue False}/>
<input type="hidden" name="isArray" value={inputValue False}/>
<input type="hidden" name="defaultValue" value="NOW()"/>
<input type="hidden" name="allowNull" value={inputValue False}/>
<input type="hidden" name="isUnique" value={inputValue False}/>
<input type="hidden" name="isReference" value={inputValue False}/>
<input type="hidden" name="withIndex" value={inputValue False}/>
<button type="submit" class="btn btn-suggested-table">
<table class="table table-sm mb-0">
<tbody>
<tr class="column">
<td style="width: 40px">+</td>
<td class="context-column column-name">updated_at</td>
<td class="context-column">TIMESTAMP WITH TIME ZONE</td>
<td class="context-column">NOW()</td>
<td class="context-column"></td>
</tr>
</tbody>
</table>
</button>
</form>
|]
userId = if hasColumn "user_id"
then Nothing
else Just [hsx|
<form method="POST" action={CreateColumnAction}>
<input type="hidden" name="tableName" value={tableName} />
<input type="hidden" name="name" value="user_id"/>
<input type="hidden" name="columnType" value="UUID"/>
<input type="hidden" name="primaryKey" value={inputValue False}/>
<input type="hidden" name="isArray" value={inputValue False}/>
<input type="hidden" name="defaultValue" value={if usesRLS then "ihp_user_id()" :: Text else ""}/>
<input type="hidden" name="allowNull" value={inputValue False}/>
<input type="hidden" name="isUnique" value={inputValue False}/>
<input type="hidden" name="isReference" value={inputValue True}/>
<input type="hidden" name="referenceTable" value="users"/>
<input type="hidden" name="autoPolicy" value={inputValue usesRLS}/>
<button type="submit" class="btn btn-suggested-table">
<table class="table table-sm mb-0">
<tbody>
<tr class="column">
<td style="width: 40px">+</td>
<td class="context-column column-name">user_id</td>
<td class="context-column">UUID</td>
<td class="context-column"></td>
<td class="context-column">FOREIGN KEY: users</td>
</tr>
</tbody>
</table>
</button>
</form>
|]
-- TODO: this should be set to True if the Schema.sql contains any RLS related code
usesRLS :: Bool
usesRLS = False
renderColumn :: Column -> Int -> Text -> [Statement] -> Html
renderColumn Column { name, columnType, defaultValue, notNull, isUnique } id tableName statements = [hsx|
<tr class="column">
<td class="context-column column-name" oncontextmenu={"showContextMenu('" <> contextMenuId <> "'); event.stopPropagation();"}><a href={EditColumnAction tableName id} class="d-block text-body nounderline">{name}</a></td>
<td class="context-column" oncontextmenu={"showContextMenu('" <> contextMenuId <> "'); event.stopPropagation();"}>{compilePostgresType columnType}{renderAllowNull}</td>
<td class="context-column" oncontextmenu={"showContextMenu('" <> contextMenuId <> "'); event.stopPropagation();"}>{renderDefault}{renderIsUnique}</td>
<td class="context-column" oncontextmenu={"showContextMenu('" <> contextMenuId <> "'); event.stopPropagation();"}>{renderPrimaryKey}{renderForeignKey}</td>
</tr>
<div class="custom-menu menu-for-column shadow backdrop-blur" id={contextMenuId}>
<a href={EditColumnAction tableName id}>Edit Column</a>
<a href={DeleteColumnAction tableName id name} class="js-delete">Delete Column</a>
<div></div>
<form action={ToggleColumnUniqueAction tableName id}>
<button type="submit" class="link-button">
{toggleButtonText}
</button>
</form>
{foreignKeyOption}
{addIndex}
<div></div>
<a href={NewColumnAction tableName}>Add Column</a>
</div>
|]
where
toggleButtonText = if isUnique then [hsx|Remove Unique|] else [hsx|Make Unique|]
contextMenuId = "context-menu-column-" <> tshow id
renderPrimaryKey = if inPrimaryKey then [hsx|PRIMARY KEY|] else mempty
inPrimaryKey = case findPrimaryKey statements tableName of
Nothing -> False
Just columnNames -> name `elem` columnNames
renderAllowNull = if notNull then mempty else [hsx|{" | " :: Text}NULL|]
renderIsUnique = if isUnique then [hsx|IS UNIQUE|] else mempty
renderDefault =
case defaultValue of
Just value -> [hsx|default: {compileExpression value} |]
Nothing -> mempty
renderForeignKey = case findForeignKey statements tableName name of
Just addConstraint@AddConstraint { constraint = ForeignKeyConstraint { name = Just constraintName, referenceTable, onDelete = onDeleteConstraint } } -> [hsx|<a href={EditForeignKeyAction tableName name constraintName referenceTable} class="d-block nounderline">FOREIGN KEY: {referenceTable} (On Delete: {maybe "" tshow onDeleteConstraint})</a>|]
_ -> mempty
foreignKeyOption = case findForeignKey statements tableName name of
Just addConstraint@AddConstraint { constraint = ForeignKeyConstraint { name = Just constraintName, referenceTable } } ->
[hsx|<a href={EditForeignKeyAction tableName name constraintName referenceTable}>Edit Foreign Key Constraint</a>
<a href={DeleteForeignKeyAction constraintName tableName} class="js-delete">Delete Foreign Key Constraint</a>|]
_ -> [hsx|<a href={NewForeignKeyAction tableName name}>Add Foreign Key Constraint</a>|]
addIndex :: Html
addIndex = unless alreadyHasIndex [hsx|
<form method="POST" action={CreateIndexAction tableName name}>
<button type="submit" class="link-button">
Add Index
</button>
</form>
|]
alreadyHasIndex :: Bool
alreadyHasIndex =
statements
|> find \case
CreateIndex { tableName = tableName', columns } -> tableName' == tableName && (VarExpression name) `elem` (map (.column) columns)
otherwise -> False
|> isJust
renderColumnIndexes tableName statements = forEachWithIndex (findTableIndexes statements tableName) renderIndex
where
renderIndex :: (Int, Statement) -> Html
renderIndex (id, index) = [hsx|
<tr class="index">
<td class="index-name" oncontextmenu={"showContextMenu('" <> contextMenuId <> "'); event.stopPropagation();"}>
<a href={EditIndexAction tableName index.indexName} class="d-block text-body nounderline">
{index.indexName}
</a>
</td>
<td class="index-expressions" oncontextmenu={"showContextMenu('" <> contextMenuId <> "'); event.stopPropagation();"}>
{unique}
</td>
<td class="index-expressions" oncontextmenu={"showContextMenu('" <> contextMenuId <> "'); event.stopPropagation();"}>
{expressions}
</td>
</tr>
<div class="custom-menu menu-for-column shadow backdrop-blur" id={contextMenuId}>
<a href={EditIndexAction tableName index.indexName}>Edit Index</a>
<a href={DeleteIndexAction tableName index.indexName} class="js-delete">Delete Index</a>
</div>
|]
where
unique = when index.unique [hsx|UNIQUE|]
showColumnOrder columnOrder =
columnOrder
|> map (\case { Asc -> "ASC"; Desc -> "DESC"; NullsFirst -> "NULLS FIRST"; NullsLast -> "NULLS LAST" })
|> unwords
expressions = index.columns
|> map (\column -> (compileExpression column.column) <> " " <> (showColumnOrder column.columnOrder))
|> intercalate ", "
contextMenuId = "context-menu-index-" <> tshow id
renderPolicies :: Text -> [Statement] -> Html
renderPolicies tableName statements = whenNonEmpty tablePolicies policiesTable
where
policiesTable = [hsx|
<section>
<h5>Policies</h5>
<table class="table table-hover table-sm">
{forEach tablePolicies renderPolicy}
</table>
</section>
|]
tablePolicies :: [Statement]
tablePolicies = statements
|> filter \case
CreatePolicy { tableName = policyTable } -> policyTable == tableName
otherwise -> False
renderPolicy policy = [hsx|
<tr class="policy">
<td class="policy-name" oncontextmenu={"showContextMenu('" <> contextMenuId <> "')"}>
<a href={EditPolicyAction tableName policyName} class="text-body nounderline">
{policy.name}
</a>
</td>
{renderExpressions policy}
</tr>
<div class="custom-menu menu-for-column shadow backdrop-blur" id={contextMenuId}>
<a href={EditPolicyAction tableName policyName}>Edit Policy</a>
<a href={DeletePolicyAction tableName policyName} class="js-delete">Delete Policy</a>
<div></div>
<a href={NewPolicyAction tableName}>Add Policy</a>
</div>
|]
where
policyName = policy.name
contextMenuId = "policy-" <> toSlug policyName
renderExpressions policy = case (policy.using, policy.check) of
(Just using, Just check) | using == check ->
[hsx|
<td class="policy-expression">
<small>read & write if</small>
{compileExpression using}
</td>
|]
(using, check) ->
[hsx|
<td class="policy-expression">
<small>read if</small>
{maybe "" compileExpression using}
</td>
<td class="policy-expression">
<small>write if</small>
{maybe "" compileExpression check}
</td>
|]
renderEnumSelector :: Text -> [(Int, Text)] -> Html
renderEnumSelector enumName values = [hsx|
<div class="col-8 column-selector" oncontextmenu="showContextMenu('context-menu-value-root')">
<div class="d-flex">
<h5>Enum Values</h5>
<div class="toolbox">
<a
href={NewEnumValueAction enumName}
class="btn btn-link btn-add mr-1"
data-toggle="tooltip"
data-placement="bottom"
title="Add Table"
>{addIcon}</a>
</div>
</div>
<table class="table table-hover table-sm">
<tbody>
{forEach values (\value -> renderValue (snd value) (fst value) enumName)}
</tbody>
</table>
</div>
<div class="custom-menu menu-for-column shadow backdrop-blur" id="context-menu-value-root">
<a href={NewEnumValueAction enumName}>Add Value</a>
</div>|]
renderValue :: Text -> Int -> Text -> Html
renderValue value valueId enumName = [hsx|
<tr class="column">
<td class="context-column column-name" oncontextmenu={"showContextMenu('" <> contextMenuId <> "'); event.stopPropagation();"}>
{value}
</td>
</tr>
<div class="custom-menu menu-for-column shadow backdrop-blur" id={contextMenuId}>
<a href={EditEnumValueAction enumName valueId}>Edit Value</a>
<a href={DeleteEnumValueAction enumName valueId} class="js-delete">Delete Value</a>
<div></div>
<a href={NewEnumValueAction enumName}>Add Value</a>
</div>
|]
where
contextMenuId = "context-menu-value-" <> tshow valueId
renderObjectSelector statements activeObjectName = [hsx|
<div class={classes ["col", "object-selector", ("empty", isEmptySelector)]} oncontextmenu="showContextMenu('context-menu-object-root')">
<div class="d-flex align-items-center pl-2">
<h5>Tables</h5>
<div class="toolbox">
<a
href={NewTableAction}
class="btn btn-link btn-add mr-1"
data-toggle="tooltip"
data-placement="bottom"
title="Add Table"
>{addIcon}</a>
</div>
</div>
{forEach tableStatements (\statement -> renderObject (snd statement) (fst statement))}
{enums}
<div class="text-muted context-menu-notice">Right click to open context menu</div>
</div>
<div class="custom-menu menu-for-table shadow backdrop-blur" id="context-menu-object-root">
<a href={NewTableAction}>Add Table</a>
<a href={NewEnumAction}>Add Enum</a>
</div>
|]
where
isEmptySelector :: Bool
isEmptySelector = statements |> map snd |> filter shouldRenderObject |> isEmpty
tableStatements :: [(Int, Statement)]
tableStatements = statements |> filter \case
(_, StatementCreateTable CreateTable {}) -> True
otherwise -> False
enumStatements :: [(Int, Statement)]
enumStatements = statements |> filter \case
(_, CreateEnumType {}) -> True
otherwise -> False
enums = whenNonEmpty enumStatements [hsx|
<div class="d-flex pl-2">
<h5>Enums</h5>
</div>
{forEach enumStatements (\statement -> renderObject (snd statement) (fst statement))}
|]
renderObject :: Statement -> Int -> Html
renderObject (StatementCreateTable CreateTable { name }) id = [hsx|
<div class={classes [("object object-table w-100 context-table pl-3", True), ("active", Just name == activeObjectName)]} oncontextmenu={"showContextMenu('" <> contextMenuId <> "'); event.stopPropagation();"}>
<div class="d-flex justify-content-between">
<a href={ShowTableAction name} class="flex-grow-1">{name}</a>
{when rlsEnabled rlsIcon}
</div>
</div>
<div class="custom-menu menu-for-table shadow backdrop-blur" id={contextMenuId}>
<a href={EditTableAction name id}>Rename Table</a>
<a href={DeleteTableAction id name} class="js-delete">Delete Table</a>
<div></div>
<a href={ShowGeneratedCodeAction name}>Show Generated Haskell Code</a>
{when controllerDoesNotExist generateControllerLink}
{unless controllerDoesNotExist openControllerLink}
<div></div>
<a href={NewColumnAction name}>Add Column to Table</a>
<div></div>
<a href={NewTableAction}>Add Table</a>
<a href={NewEnumAction}>Add Enum</a>
</div>
|]
where
contextMenuId = "context-menu-" <> tshow id
generateControllerLink = [hsx|<a href={pathTo NewControllerAction <> "?name=" <> name}>Generate Controller</a>|]
openControllerLink = [hsx|<a href={pathTo OpenControllerAction <> "?name=" <> name} target="_blank">Open Controller</a>|]
controllerDoesNotExist = not $ (ucfirst name) `elem` webControllers
(WebControllers webControllers) = fromFrozenContext @WebControllers
rlsEnabled = statements
|> map snd
|> find \case
EnableRowLevelSecurity { tableName = rlsTable } -> rlsTable == name
otherwise -> False
|> isJust
rlsIcon = [hsx|
<span
class="rls-enabled"
data-toggle="tooltip"
data-placement="right"
data-html="true"
title="Row Level Security enabled"
>{shieldIcon}</span>
|]
renderObject CreateEnumType { name } id = [hsx|
<a href={ShowEnumAction name} class={classes [("object object-table w-100 context-enum pl-3", True), ("active", Just name == activeObjectName)]} oncontextmenu={"showContextMenu('" <> contextMenuId <> "'); event.stopPropagation();"}>
<div class="d-flex">
{name}
</div>
</a>
<div class="custom-menu menu-for-enum shadow backdrop-blur" id={contextMenuId}>
<a href={EditEnumAction name id}>Rename Enum</a>
<a href={DeleteEnumAction id} class="js-delete">Delete Enum</a>
<div></div>
<a href={ShowGeneratedCodeAction name}>Show Generated Haskell Code</a>
<a href={NewEnumValueAction name}>Add Column to Table</a>
<div></div>
<a href={NewTableAction}>Add Table</a>
<a href={NewEnumAction}>Add Enum</a>
</div>
|]
where
contextMenuId = "context-menu-" <> tshow id
renderObject Comment {} id = mempty
renderObject AddConstraint {} id = mempty
renderObject CreateExtension {} id = mempty
renderObject CreateIndex {} id = mempty
renderObject CreateFunction {} id = mempty
renderObject UnknownStatement {} id = mempty
renderObject EnableRowLevelSecurity {} id = mempty
renderObject CreatePolicy {} id = mempty
shouldRenderObject (StatementCreateTable CreateTable {}) = True
shouldRenderObject CreateEnumType {} = True
shouldRenderObject _ = False
removeQuotes :: [Char] -> Text
removeQuotes (x:xs) = cs $ fromMaybe [] (init xs)
removeQuotes n = cs n
findForeignKey :: [Statement] -> Text -> Text -> Maybe Statement
findForeignKey statements tableName columnName =
find
(\case
AddConstraint { tableName = fkTable, constraint = ForeignKeyConstraint { columnName = fkColumn } } -> tableName == fkTable && columnName == fkColumn
otherwise -> False
)
statements
findPrimaryKey :: [Statement] -> Text -> Maybe [Text]
findPrimaryKey statements tableName = do
(StatementCreateTable createTable) <- find (isCreateTable tableName) statements
pure . primaryKeyColumnNames $ createTable.primaryKeyConstraint
where
isCreateTable tableName (StatementCreateTable CreateTable { name }) = name == tableName
isCreateTable _ _ = False
findTableIndex :: [Statement] -> Text -> Maybe Statement
findTableIndex statements tableName =
find (\case CreateIndex { tableName = tableName' } -> tableName' == tableName; otherwise -> False) statements
findTableIndexes :: [Statement] -> Text -> [Statement]
findTableIndexes statements tableName =
filter (\case CreateIndex { tableName = tableName' } -> tableName' == tableName; otherwise -> False) statements
replace :: Int -> a -> [a] -> [a]
replace i e xs = case List.splitAt i xs of
(before, _:after) -> before ++ (e: after)
(a, b) -> a ++ b
-- | https://github.com/postgres/pgadmin4/blob/master/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/static/img/table.svg
tableIcon = preEscapedToHtml [plain|<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="24" height="24"><defs><style>.cls-table-1{fill:#f2f2f2;}.cls-table-2{fill:#2195e7;}.cls-table-3{fill:none;stroke:#c1cbd5;stroke-linejoin:round;}.cls-table-3,.cls-table-4{stroke-width:0.75px;}.cls-table-4{fill:#def4fd;stroke:#2195e7;stroke-miterlimit:1;}</style></defs><title>table</title><g id="_2" data-name="2"><rect class="cls-table-1" x="2.92" y="3.65" width="10.15" height="8.71" rx="0.53" ry="0.53"/><path class="cls-table-2" d="M12.55,4a.15.15,0,0,1,.15.15v7.66a.15.15,0,0,1-.15.15H3.45a.15.15,0,0,1-.15-.15V4.17A.15.15,0,0,1,3.45,4h9.1m0-.75H3.45a.9.9,0,0,0-.9.9v7.66a.9.9,0,0,0,.9.9h9.1a.9.9,0,0,0,.9-.9V4.17a.9.9,0,0,0-.9-.9Z"/><line class="cls-table-3" x1="3.32" y1="9.43" x2="12.69" y2="9.43"/><line class="cls-table-3" x1="8.01" y1="7.09" x2="8" y2="11.97"/><line class="cls-table-4" x1="8.01" y1="4.03" x2="8" y2="6.58"/><line class="cls-table-4" x1="12.68" y1="6.74" x2="3.32" y2="6.74"/></g></svg>|]
-- | https://github.com/postgres/pgadmin4/blob/master/web/pgadmin/browser/server_groups/servers/databases/schemas/types/static/img/type.svg
enumIcon = preEscapedToHtml [plain|<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" width="24" height="24"><defs><style>.cls-enum-1{fill:#d6ffea;}.cls-enum-2{fill:#5acb9a;}</style></defs><title>type</title><g id="_2" data-name="2"><path class="cls-enum-1" d="M3.9,3.47h8.19a.44.44,0,0,1,.44.44V12.1a.43.43,0,0,1-.43.43H3.91a.44.44,0,0,1-.44-.44V3.9A.43.43,0,0,1,3.9,3.47Z"/><path class="cls-enum-2" d="M12.1,3.85a.06.06,0,0,1,.06.06V12.1a.06.06,0,0,1-.06.06H3.9a.06.06,0,0,1-.06-.06V3.91a.06.06,0,0,1,.06-.06H12.1m0-.75H3.9a.81.81,0,0,0-.81.81V12.1a.81.81,0,0,0,.81.81H12.1a.81.81,0,0,0,.81-.81V3.91a.81.81,0,0,0-.81-.81Z"/></g></svg>|]
-- | https://github.com/postgres/pgadmin4/blob/master/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/indexes/static/img/index.svg
indexIcon = preEscapedToHtml [plain|<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-index-1,.cls-index-3{fill:#def4fd;}.cls-index-2{fill:#357fd3;}.cls-index-3,.cls-index-4{stroke:#357fd3;stroke-linecap:round;stroke-linejoin:round;stroke-width:0.75px;}.cls-index-4{fill:none;}</style></defs><title>index</title><g id="_8" data-name="8"><path class="cls-index-1" d="M5.81,3.37h4.39a.44.44,0,0,1,.44.44V5.49a.44.44,0,0,1-.44.44H5.8a.43.43,0,0,1-.43-.43V3.81A.44.44,0,0,1,5.81,3.37Z"/><path class="cls-index-2" d="M10.2,3.75a.06.06,0,0,1,.06.06V5.49a.06.06,0,0,1-.06.06H5.8a.06.06,0,0,1-.06-.06V3.81a.06.06,0,0,1,.06-.06H10.2m0-.75H5.8A.81.81,0,0,0,5,3.81V5.49a.81.81,0,0,0,.81.81H10.2A.81.81,0,0,0,11,5.49V3.81A.81.81,0,0,0,10.2,3Z"/><rect class="cls-index-1" x="11.38" y="10.57" width="2.24" height="2.06" rx="0.44" ry="0.44"/><path class="cls-index-2" d="M13.19,10.94a.06.06,0,0,1,.06.06v1.19a.06.06,0,0,1-.06.06H11.81a.06.06,0,0,1-.06-.06V11a.06.06,0,0,1,.06-.06h1.37m0-.75H11.81A.81.81,0,0,0,11,11v1.19a.81.81,0,0,0,.81.81h1.37a.81.81,0,0,0,.81-.81V11a.81.81,0,0,0-.81-.81Z"/><rect class="cls-index-1" x="2.38" y="10.57" width="2.24" height="2.06" rx="0.44" ry="0.44"/><path class="cls-index-2" d="M4.19,10.94a.06.06,0,0,1,.06.06v1.19a.06.06,0,0,1-.06.06H2.81a.06.06,0,0,1-.06-.06V11a.06.06,0,0,1,.06-.06H4.19m0-.75H2.81A.81.81,0,0,0,2,11v1.19a.81.81,0,0,0,.81.81H4.19A.81.81,0,0,0,5,12.19V11a.81.81,0,0,0-.81-.81Z"/><rect class="cls-index-1" x="6.88" y="10.57" width="2.24" height="2.06" rx="0.44" ry="0.44"/><path class="cls-index-2" d="M8.69,10.94a.06.06,0,0,1,.06.06v1.19a.06.06,0,0,1-.06.06H7.32a.06.06,0,0,1-.06-.06V11a.06.06,0,0,1,.06-.06H8.69m0-.75H7.32a.81.81,0,0,0-.81.81v1.19a.81.81,0,0,0,.81.81H8.69a.81.81,0,0,0,.81-.81V11a.81.81,0,0,0-.81-.81Z"/><line class="cls-index-3" x1="7.98" y1="6.3" x2="7.98" y2="8.54"/><line class="cls-index-3" x1="7.98" y1="8.54" x2="7.98" y2="10.19"/><polyline class="cls-index-4" points="12.5 10.19 12.5 8.17 3.5 8.17 3.5 10.19"/></g></svg>|]
-- | https://github.com/postgres/pgadmin4/blob/master/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/index_constraint/static/img/unique_constraint.svg
uniqueIndexIcon = preEscapedToHtml [plain|<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-unique-index-1{fill:#cbe7f6;}.cls-unique-index-1,.cls-unique-index-2{stroke:#2c66bd;stroke-linecap:round;stroke-linejoin:round;stroke-width:0.75px;}.cls-unique-index-2{font-size:8px;fill:#2c66bd;font-family:Georgia, Georgia;}</style></defs><title>unique index</title><g id="_2" data-name="2"><circle class="cls-unique-index-1" cx="8" cy="8" r="5.4"/><text class="cls-unique-index-2" transform="translate(6.28 10.17)">1</text></g></svg>|]
-- | https://github.com/postgres/pgadmin4/blob/master/web/pgadmin/browser/server_groups/servers/databases/schemas/tables/constraints/check_constraint/static/img/check-constraint.svg
constraintIcon = preEscapedToHtml [plain|<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-constraint-1{fill:none;stroke:#719317;stroke-linecap:round;stroke-linejoin:round;stroke-width:2px;}</style></defs><title>constraint</title><g id="_2" data-name="2"><polyline class="cls-constraint-1" points="4 9.25 6.5 11.75 12 4.25"/></g></svg>|]
-- | https://github.com/postgres/pgadmin4/blob/master/web/pgadmin/browser/server_groups/servers/databases/languages/static/img/language.svg
commentIcon = preEscapedToHtml [plain|<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-comment-1{fill:#f8f8f8;stroke:#d5d4d5;stroke-linejoin:round;stroke-width:0.75px;}</style></defs><title>comment</title><g id="_2" data-name="2"><path class="cls-comment-1" d="M13.78,7c0,.62-.57,1.19-1.49,1.61-.39.18.07,2.59-.43,2.72s-2-2-2.53-2c-.42,0-.86.06-1.32.06C4.81,9.44,2.23,8.36,2.23,7S4.81,4.63,8,4.63,13.78,5.71,13.78,7Z"/></g></svg>|]
-- | https://github.com/postgres/pgadmin4/blob/master/web/pgadmin/browser/server_groups/servers/databases/extensions/static/img/extension.svg
extensionIcon = preEscapedToHtml [plain|<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-extension-1,.cls-extension-4{fill:#c9edd2;}.cls-extension-2{fill:#1a6016;}.cls-extension-3{fill:none;}.cls-extension-3,.cls-extension-4{stroke:#1a6016;stroke-linejoin:round;stroke-width:0.75px;}.cls-extension-4{stroke-linecap:round;}</style></defs><title>extension</title><g id="_2" data-name="2"><path class="cls-extension-1" d="M5.62,13.13A.62.62,0,0,1,5,12.51v-9a.62.62,0,0,1,.62-.62h6.61a.62.62,0,0,1,.62.62v6.66l-3,3Z"/><path class="cls-extension-2" d="M12.23,3.25a.24.24,0,0,1,.24.24V10L9.72,12.75H5.62a.24.24,0,0,1-.24-.24v-9a.24.24,0,0,1,.24-.24h6.61m0-.75H5.62a1,1,0,0,0-1,1v9a1,1,0,0,0,1,1H10l3.19-3.19V3.49a1,1,0,0,0-1-1Z"/><polyline class="cls-extension-3" points="13 9.52 9.49 9.54 9.49 13.5"/><rect class="cls-extension-4" x="2.78" y="5.13" width="6.01" height="2.01"/></g></svg>|]
-- | https://github.com/postgres/pgadmin4/blob/master/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/static/img/function.svg
functionIcon = preEscapedToHtml [plain|<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-function-1{fill:#edfffd;}.cls-function-2{fill:#34495e;stroke-miterlimit:10;stroke-width:0.5px;}.cls-function-2,.cls-function-3{stroke:#2f91a3;}.cls-function-3{fill:#2980b9;stroke-linecap:round;stroke-linejoin:round;stroke-width:0.75px;}</style></defs><title>function</title><g id="_2" data-name="2"><polygon class="cls-function-1" points="10.99 13.41 12.26 12.74 12.61 11.24 12.61 10.54 12.8 9.04 13.24 8.48 14.37 7.92 13.24 7.92 13.24 7.92 13.24 7.92 14.02 7.78 12.85 6.91 12.78 6.32 12.78 5.24 12.61 3.76 12.14 3.21 11.39 2.88 10.99 2.84 4.93 2.72 4.06 3.33 3.52 3.77 3.52 4.38 3.61 4.21 3.42 6.23 3.52 6.93 2 7.92 1.78 8.11 2 7.92 2.98 8.63 3.12 9.04 3.7 10.56 3.67 11.22 3.65 11.68 3.67 10.88 3.67 11.27 3.89 12.23 4.24 12.71 4.87 13.09 5.37 13.41 10.99 13.41"/><path class="cls-function-2" d="M10.57,13.2a2.46,2.46,0,0,0,1.14-.28,1.56,1.56,0,0,0,.59-.6,2,2,0,0,0,.22-.78q0-.42,0-.81v.2c0-.38,0-.7,0-1a5.15,5.15,0,0,1,.06-.67,1.53,1.53,0,0,1,.14-.46A1.32,1.32,0,0,1,13,8.51a1.4,1.4,0,0,1,.34-.26,2.21,2.21,0,0,1,.36-.15A2,2,0,0,1,14,8c.1,0,.1-.05,0-.07a2,2,0,0,1-.33-.07,2,2,0,0,1-.36-.15A1.4,1.4,0,0,1,13,7.49a1.34,1.34,0,0,1-.24-.33,1.54,1.54,0,0,1-.14-.46A5.17,5.17,0,0,1,12.56,6q0-.39,0-1v.2c0-.26,0-.53,0-.81a2,2,0,0,0-.22-.78,1.57,1.57,0,0,0-.59-.6,2.48,2.48,0,0,0-1.14-.28V2.55l.3,0a2.29,2.29,0,0,1,.37.06,2.84,2.84,0,0,1,.4.13A1.61,1.61,0,0,1,12,3a1.85,1.85,0,0,1,.46.5,2,2,0,0,1,.24.59,3.87,3.87,0,0,1,.09.72c0,.26,0,1,0,1a4.39,4.39,0,0,0,.07.83,1.32,1.32,0,0,0,.26.59,1.47,1.47,0,0,0,.55.41.38.38,0,0,1,0,.65,1.48,1.48,0,0,0-.55.4,1.33,1.33,0,0,0-.26.59,4.39,4.39,0,0,0-.07.83s-.05,1.54-.1,1.76a2,2,0,0,1-.24.59A1.84,1.84,0,0,1,12,13a1.61,1.61,0,0,1-.39.23,3,3,0,0,1-.4.13,2.41,2.41,0,0,1-.37.06l-.3,0Z"/><path class="cls-function-2" d="M5.66,2.8a2.46,2.46,0,0,0-1.14.28,1.56,1.56,0,0,0-.59.6,2,2,0,0,0-.22.78q0,.42,0,.81v-.2c0,.38,0,.7,0,1a5.15,5.15,0,0,1-.06.67,1.53,1.53,0,0,1-.14.46,1.32,1.32,0,0,1-.24.33,1.4,1.4,0,0,1-.34.26,2.21,2.21,0,0,1-.36.15A2,2,0,0,1,2.18,8c-.1,0-.1.05,0,.07a2,2,0,0,1,.33.07,2,2,0,0,1,.36.15,1.4,1.4,0,0,1,.34.26,1.34,1.34,0,0,1,.24.33,1.54,1.54,0,0,1,.14.46,5.17,5.17,0,0,1,.06.67q0,.39,0,1v-.2c0,.26,0,.53,0,.81a2,2,0,0,0,.22.78,1.57,1.57,0,0,0,.59.6,2.48,2.48,0,0,0,1.14.28v.24l-.3,0A2.29,2.29,0,0,1,5,13.36a2.84,2.84,0,0,1-.4-.13A1.61,1.61,0,0,1,4.2,13a1.85,1.85,0,0,1-.46-.5,2,2,0,0,1-.24-.59,3.87,3.87,0,0,1-.09-.72c0-.26,0-1,0-1a4.39,4.39,0,0,0-.07-.83,1.32,1.32,0,0,0-.26-.59,1.47,1.47,0,0,0-.55-.41.38.38,0,0,1,0-.65,1.48,1.48,0,0,0,.55-.4,1.33,1.33,0,0,0,.26-.59,4.39,4.39,0,0,0,.07-.83s.05-1.54.1-1.76a2,2,0,0,1,.24-.59A1.84,1.84,0,0,1,4.2,3a1.61,1.61,0,0,1,.39-.23A3,3,0,0,1,5,2.64a2.41,2.41,0,0,1,.37-.06l.3,0Z"/><line class="cls-function-3" x1="6.32" y1="5.41" x2="10.07" y2="5.41"/><line class="cls-function-3" x1="6.32" y1="7.91" x2="10.07" y2="7.91"/><line class="cls-function-3" x1="6.32" y1="10.41" x2="8.82" y2="10.41"/></g></svg>|]
-- | https://github.com/postgres/pgadmin4/blob/master/web/pgadmin/misc/static/explain/img/ex_unknown.svg
unknownIcon = preEscapedToHtml [plain|<svg id="_1" data-name="1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><defs><style>.cls-unknown-1{fill:#1cafe4;}</style></defs><title>unknown</title><path class="cls-unknown-1" d="M23.34,13.2a14.76,14.76,0,0,1,8.16-2.14,17.57,17.57,0,0,1,10.56,3q4.2,3,4.2,9a10.18,10.18,0,0,1-1.82,6.16,21,21,0,0,1-4.09,3.88l-2,1.55a6,6,0,0,0-2.16,3A12.5,12.5,0,0,0,35.83,41H28.2a22.11,22.11,0,0,1,.9-6.57,11.57,11.57,0,0,1,3.76-4.18l2-1.6A7.17,7.17,0,0,0,36.53,27a5.67,5.67,0,0,0,1.12-3.4,6.58,6.58,0,0,0-1.25-3.9q-1.25-1.76-4.56-1.76t-4.62,2.17a8.34,8.34,0,0,0-1.36,4.5H17.74Q18.08,16.55,23.34,13.2ZM28,44.81h8.41v8.13H28Z"/></svg>|]
-- | https://fonts.google.com/icons?icon.query=shield
shieldIcon = preEscapedToHtml [plain|<svg xmlns="http://www.w3.org/2000/svg" enable-background="new 0 0 24 24" height="1rem" viewBox="0 0 24 24" width="1rem" fill="currentColor"><g><rect fill="none" height="24" width="24"/></g><g><path d="M12,2L4,5v6.09c0,5.05,3.41,9.76,8,10.91c4.59-1.15,8-5.86,8-10.91V5L12,2z M18,11.09c0,4-2.55,7.7-6,8.83 c-3.45-1.13-6-4.82-6-8.83v-4.7l6-2.25l6,2.25V11.09z"/></g></svg>|]