Skip to content

Commit

Permalink
Mermaid.js schema view
Browse files Browse the repository at this point in the history
  • Loading branch information
kyleu committed May 17, 2018
1 parent 585a7b6 commit 59311f4
Show file tree
Hide file tree
Showing 12 changed files with 277 additions and 18 deletions.
174 changes: 174 additions & 0 deletions app/assets/stylesheets/mermaid.less
@@ -0,0 +1,174 @@
// Variables
@mainBkg: #fff;
@secondBkg: #ddd;
@lineColor: #333;
@border1: #333;
@border2: #aaa;
@arrowheadColor: #333333;

// Flowchart variables
@nodeBkg: @mainBkg;
@nodeBorder: #333;
@clusterBkg: @secondBkg;
@clusterBorder: @border2;
@defaultLinkColor: @lineColor;
@titleColor: #333;
@edgeLabelBackground: #e8e8e8;


// Flowchart
.label {
font-family: "Roboto";
color: #333;
}

.node rect,
.node circle,
.node ellipse,
.node polygon {
fill: @mainBkg;
stroke: @nodeBorder;
stroke-width: 1px;
}

.node.clickable {
cursor: pointer;
}

.arrowheadPath {
fill: @arrowheadColor;
}

.edgePath .path {
stroke: @lineColor;
stroke-width: 1.5px;
}

.edgeLabel {
background-color: @edgeLabelBackground;
}

.cluster rect {
fill: @secondBkg !important;
stroke: @clusterBorder !important;
stroke-width: 1px !important;
}

.cluster text {
fill: @titleColor;
}

div.mermaidTooltip {
position: absolute;
text-align: center;
max-width: 200px;
padding: 2px;
font-family: "Roboto";
font-size: 12px;
background: @secondBkg;
border: 1px solid @border2;
border-radius: 2px;
pointer-events: none;
z-index: 100;
}

// Class
g.classGroup text {
fill: @nodeBorder;
stroke: none;
font-family: "Roboto";
font-size: 10px;
}

g.classGroup rect {
fill: @nodeBkg;
stroke: @nodeBorder;
}

g.classGroup line {
stroke: @nodeBorder;
stroke-width: 1;
}

.classLabel .box {
stroke: none;
stroke-width: 0;
fill: @nodeBkg;
opacity: 0.5;
}

.classLabel .label {
fill: @nodeBorder;
font-size: 10px;
}

.relation {
stroke: @nodeBorder;
stroke-width: 1;
fill: none;
}

.composition {
fill: @nodeBorder;
stroke: @nodeBorder;
stroke-width: 1;
}

#compositionStart {
.composition;
}

#compositionEnd {
.composition;
}

.aggregation {
fill: @nodeBkg;
stroke: @nodeBorder;
stroke-width: 1;
}

#aggregationStart {
.aggregation;
}

#aggregationEnd {
.aggregation;
}

#dependencyStart {
.composition;
}

#dependencyEnd {
.composition;
}

#extensionStart {
.composition;
}

#extensionEnd {
.composition;
}

// Custom
.mermaid-container {
height: 100%;
overflow: hidden;
}

.mermaid {
height: 100%;
svg {
height: 100%;
}
h6 {
font-size: 125%;
margin: 0 0 6px 0;
}
span {
float: right;
margin-left: 6px;
}
}
28 changes: 14 additions & 14 deletions app/controllers/admin/ScalaExportController.scala
Expand Up @@ -70,8 +70,7 @@ class ScalaExportController @javax.inject.Inject() (override val ctx: Applicatio
)

val x = config.asJson.spaces2
val f = s"./tmp/${schema.id}.json".toFile
f.overwrite(x)
configFileFor(schema).overwrite(x)

ScalaExportService(config).export(persist = true).map { result =>
Ok(views.html.admin.scalaExport.export(result.er, result.files, result.out))
Expand All @@ -82,24 +81,25 @@ class ScalaExportController @javax.inject.Inject() (override val ctx: Applicatio
}

private[this] def getConfig(schema: Schema): ExportConfiguration = {
val schemaId = ExportHelper.toIdentifier(schema.catalog.orElse(schema.schemaName).getOrElse(schema.username))

val overrides = "./tmp/locations.txt".toFile.lines.map { l =>
l.split('=').toList match {
case h :: t :: Nil => h.trim -> t.trim
case _ => throw new IllegalStateException(s"Unhandled line [$l].")
}
}.toMap

val f = (if (overrides.contains(schemaId)) { overrides(schemaId) + "/databaseflow.json" } else { s"./tmp/$schemaId.json" }).toFile

val f = configFileFor(schema)
if (f.exists) {
ScalaExportHelper.merge(schema, decodeJson[ExportConfiguration](f.contentAsString) match {
case Right(x) => x
case Left(x) => throw x
})
} else {
ExportConfigurationDefault.forSchema(schemaId, schema)
ExportConfigurationDefault.forSchema(schema)
}
}

private[this] def configFileFor(schema: Schema) = {
val schemaId = ExportHelper.toIdentifier(schema.id)
val overrides = "./tmp/locations.txt".toFile.lines.map { l =>
l.split('=').toList match {
case h :: t :: Nil => h.trim -> t.trim
case _ => throw new IllegalStateException(s"Unhandled line [$l].")
}
}.toMap
(if (overrides.contains(schemaId)) { overrides(schemaId) + "/databaseflow.json" } else { s"./tmp/$schemaId.json" }).toFile
}
}
2 changes: 1 addition & 1 deletion app/controllers/admin/ScalaExportHelper.scala
Expand Up @@ -17,7 +17,7 @@ object ScalaExportHelper {
config.getModelOpt(t.name) match {
case Some(m) =>
val fields = t.columns.zipWithIndex.map { c =>
m.fields.find(_.columnName == c._1.name).getOrElse(ExportConfigurationDefault.loadField(c._1, c._2, enums = config.enums))
m.fields.find(_.columnName == c._1.name).getOrElse(ExportConfigurationDefault.loadField(c._1, c._2, enums = enums))
}
m.copy(fields = fields.toList)
case None => ExportConfigurationDefault.loadModel(schema, t, enums)
Expand Down
19 changes: 19 additions & 0 deletions app/controllers/schema/SchemaController.scala
@@ -0,0 +1,19 @@
package controllers.schema

import controllers.BaseController
import services.connection.ConnectionSettingsService
import services.schema.{MermaidChartService, SchemaService}
import util.ApplicationContext

import util.FutureUtils.defaultContext

@javax.inject.Singleton
class SchemaController @javax.inject.Inject() (override val ctx: ApplicationContext) extends BaseController {
def chart(id: String) = withSession("detail." + id) { implicit request =>
val conn = ConnectionSettingsService.connFor(id).getOrElse(throw new IllegalStateException(s"Invalid connection [$id]"))
SchemaService.getSchemaWithDetails(Some(request.identity), conn).map { schema =>
val chartData = MermaidChartService.chartFor(schema)
Ok(views.html.schema.mermaid(request.identity, id, conn.name, chartData))
}
}
}
20 changes: 20 additions & 0 deletions app/services/schema/MermaidChartService.scala
@@ -0,0 +1,20 @@
package services.schema

import models.schema.Schema

object MermaidChartService {
def chartFor(s: Schema) = {
val ret = new StringBuilder("graph TD\n")
def addLine(l: String) = ret.append(" " + l + "\n")
s.tables.foreach { t =>
val cols = t.columns.map(c => s"""<div><span>${c.columnType}</span>${c.name}</div>""").mkString
addLine(s"""${t.name}["<h6>fa:fa-folder-o ${t.name}</h6>$cols"]""")
}
s.tables.foreach { t =>
t.foreignKeys.foreach { fk =>
addLine(s"""${t.name} --> ${fk.targetTable}""")
}
}
ret.toString
}
}
2 changes: 1 addition & 1 deletion app/views/connection/list.scala.html
Expand Up @@ -22,7 +22,7 @@
<i class="fa @models.template.Icons.adHocQuery" style="font-size: 1rem;"></i>
@messages("query.title")
</a>
<a href="@controllers.graphql.routes.SchemaController.voyager(c.slug)" class="waves-effect waves-light btn theme" style="margin: 10px 10px 10px 0;">
<a href="@controllers.schema.routes.SchemaController.chart(c.slug)" class="waves-effect waves-light btn theme" style="margin: 10px 10px 10px 0;">
<i class="fa @models.template.Icons.schema" style="font-size: 1rem;"></i>
@messages("schema.title")
</a>
Expand Down
32 changes: 32 additions & 0 deletions app/views/schema/mermaid.scala.html
@@ -0,0 +1,32 @@
@(user: models.user.User, connectionId: String, connectionName: String, chartData: String)(
implicit request: Request[AnyContent], session: Session, flash: Flash, messages: Messages
)@layout.simple(
user = Some(user),
title = connectionName + " Schema",
mainDivClass = "mermaid-container",
scripts = Seq(
routes.Assets.versioned("vendor/mermaid/mermaid.min.js").url,
routes.Assets.versioned("vendor/mermaid/svg-pan-zoom.min.js").url
),
stylesheets = Seq(routes.Assets.versioned("stylesheets/mermaid.min.css").url)
) {
<div class="mermaid">@Html(chartData)</div>
<script>
$(document).ready(function() {
mermaid.flowchartConfig = {
width: "100%"
};
mermaid.initialize({
theme: null,
themeCSS: "",
flowchart: {
//curve: 'basis'
}
});
setTimeout(function() {
svgPanZoom(".mermaid svg");
}, 2000)

});
</script>
}
1 change: 1 addition & 0 deletions conf/routes
Expand Up @@ -12,6 +12,7 @@ GET /c/:id controllers.connection.Conn
GET /c/:id/delete controllers.connection.ConnectionSettingsController.delete(id: java.util.UUID)
GET /c/:id/cp controllers.connection.ConnectionSettingsController.copyConnection(id: java.util.UUID)
POST /c/:id/test controllers.connection.ConnectionTestController.test(id: java.util.UUID)
GET /c/:id/schema controllers.schema.SchemaController.chart(id)

# Query Interface
GET /q/:conn controllers.query.QueryController.main(conn)
Expand Down
9 changes: 9 additions & 0 deletions public/vendor/mermaid/mermaid.min.js

Large diffs are not rendered by default.

3 changes: 3 additions & 0 deletions public/vendor/mermaid/svg-pan-zoom.min.js

Large diffs are not rendered by default.

Expand Up @@ -30,7 +30,7 @@ object ExportFieldGraphQL {

case EnumType => field.enumOpt match {
case Some(enum) => enum.propertyName + "EnumType"
case None => throw new IllegalStateException(s"Cannot load enum.")
case None => throw new IllegalStateException(s"Cannot load enum for field [${field.propertyName}].")
}
case CodeType => "StringType"
case TagsType => "TagsType"
Expand Down
Expand Up @@ -6,7 +6,8 @@ import services.scalaexport.ExportHelper
import services.scalaexport.ExportHelper.{toClassName, toDefaultTitle, toIdentifier}

object ExportConfigurationDefault {
def forSchema(key: String, schema: Schema) = {
def forSchema(schema: Schema) = {
val key = ExportHelper.toIdentifier(schema.id)
val enums = schema.enums.map { e =>
val pkg = e.key match {
case "setting_key" => List("settings")
Expand Down

0 comments on commit 59311f4

Please sign in to comment.