rails-mermaid_erd 0.1.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +59 -0
- data/Rakefile +3 -0
- data/lib/assets/logo.svg +15 -0
- data/lib/rails-mermaid_erd/version.rb +3 -0
- data/lib/rails-mermaid_erd.rb +202 -0
- data/lib/templates/index.html.erb +584 -0
- metadata +65 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA256:
|
3
|
+
metadata.gz: 05ed1d4ef4a6d7343bd286c7644ad8e5b41acd27743dc5af7d387a919c2fa4a8
|
4
|
+
data.tar.gz: 0f88664e5879bdbd2a3ff79c01976cf016871a8ab8b16af777d6a19a5d78ba87
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: 430cd950e08598728145f1f51c83a5a35700e82c4e2f5274b8b68906e0ed1c7f67a44046ce0eee533b57c17623367149173c6385c884183f0e395cb04601089a
|
7
|
+
data.tar.gz: 39d7fee31257a11024666023100c3ac55e3d2b17513f48fbff5bd8da305acc84bd36d538cb47fd32c649315e0e650e0ce623d2df3d6acc0845c05fa06982f31b
|
data/MIT-LICENSE
ADDED
@@ -0,0 +1,20 @@
|
|
1
|
+
Copyright 2022
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,59 @@
|
|
1
|
+
# Rails Mermaid ERD
|
2
|
+
|
3
|
+
Generate [Mermaid ERD](https://mermaid-js.github.io/mermaid/#/entityRelationshipDiagram) from your Ruby on Rails application.
|
4
|
+
|
5
|
+
[<img src="./docs/screen_shot.png" width="50%">](./docs/screen_shot.png)
|
6
|
+
|
7
|
+
[Demo Page](https://koedame.github.io/rails-mermaid_erd/example.html)
|
8
|
+
|
9
|
+
Mermaid ERD can be generated at will.
|
10
|
+
The generated ERD can be copied in Markdown format, so they can be easily shared on GitHub.
|
11
|
+
You can also save it as an image, so it can be used in environments where Mermaid is not available.
|
12
|
+
The editor is a single HTML file, so the entire editor can be shared.
|
13
|
+
|
14
|
+
## Installation
|
15
|
+
|
16
|
+
Add this line to your application's Gemfile:
|
17
|
+
|
18
|
+
```ruby
|
19
|
+
gem "rails-mermaid_erd", group: :development
|
20
|
+
```
|
21
|
+
|
22
|
+
And then execute:
|
23
|
+
|
24
|
+
```bash
|
25
|
+
$ bundle install
|
26
|
+
```
|
27
|
+
|
28
|
+
## Usage
|
29
|
+
|
30
|
+
Run rake task `mermaid_erd` will generate `<app_root>/mermaid_erd/index.html`.
|
31
|
+
|
32
|
+
```bash
|
33
|
+
$ bundle exec rails mermaid_erd
|
34
|
+
# or
|
35
|
+
$ bundle exec rake mermaid_erd
|
36
|
+
```
|
37
|
+
|
38
|
+
Simply open the generated `<app_root>/mermaid_erd/index.html` in your browser.
|
39
|
+
|
40
|
+
This file is not required for Git management, so you can add it to `.gitignore` if necessary
|
41
|
+
|
42
|
+
```.gitignore
|
43
|
+
mermaid_erd
|
44
|
+
```
|
45
|
+
|
46
|
+
`<app_root>/mermaid_erd/index.html` is single HTML file.
|
47
|
+
If you share this file, it can be used by those who do not have Ruby on Rails environment. Or, you can upload the file to a web server and share it with the same URL.
|
48
|
+
|
49
|
+
It would be very smart to generate it automatically using CI.
|
50
|
+
|
51
|
+
<!--
|
52
|
+
TODO:
|
53
|
+
## Contributing
|
54
|
+
|
55
|
+
Contribution directions go here.
|
56
|
+
-->
|
57
|
+
|
58
|
+
## License
|
59
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
data/Rakefile
ADDED
data/lib/assets/logo.svg
ADDED
@@ -0,0 +1,15 @@
|
|
1
|
+
<svg width="276" height="276" viewBox="0 0 276 276" fill="none" xmlns="http://www.w3.org/2000/svg" class="h-6 w-6">
|
2
|
+
<g clip-path="url(#clip0_3_2)">
|
3
|
+
<rect width="276" height="276" rx="40" fill="#DC2626"/>
|
4
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M94 60.88C94 54.8443 98.9263 50 104.943 50H170.313C176.33 50 181.208 54.8929 181.208 60.9286L181.257 107.071C181.257 113.107 176.379 118 170.362 118C151.8 118 123.554 118 104.992 118C98.9748 118 94.0485 113.058 94.0485 107.023L94 60.88Z" fill="white"/>
|
5
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M32 168.88C32 162.844 36.9263 158 42.9434 158H108.313C114.33 158 119.208 162.893 119.208 168.929L119.257 215.071C119.257 221.107 114.379 226 108.362 226C89.8 226 61.5537 226 42.9919 226C36.9748 226 32.0485 221.058 32.0485 215.023L32 168.88Z" fill="white"/>
|
6
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M156 168.88C156 162.844 160.926 158 166.943 158H232.313C238.33 158 243.208 162.893 243.208 168.929L243.257 215.071C243.257 221.107 238.379 226 232.362 226C213.8 226 185.554 226 166.992 226C160.975 226 156.048 221.058 156.048 215.023L156 168.88Z" fill="white"/>
|
7
|
+
<path d="M134.142 109.071L141.213 102L226.066 186.853L218.995 193.924L134.142 109.071Z" fill="white"/>
|
8
|
+
<path d="M133.853 102L140.924 109.071L56.0711 193.924L49 186.853L133.853 102Z" fill="white"/>
|
9
|
+
</g>
|
10
|
+
<defs>
|
11
|
+
<clipPath id="clip0_3_2">
|
12
|
+
<rect width="276" height="276" fill="white"/>
|
13
|
+
</clipPath>
|
14
|
+
</defs>
|
15
|
+
</svg>
|
@@ -0,0 +1,202 @@
|
|
1
|
+
require "erb"
|
2
|
+
require "fileutils"
|
3
|
+
require "rake"
|
4
|
+
require "rake/dsl_definition"
|
5
|
+
require "rails-mermaid_erd/version"
|
6
|
+
|
7
|
+
module RailsMermaidErd
|
8
|
+
extend Rake::DSL
|
9
|
+
|
10
|
+
desc "Generate Mermaid ERD."
|
11
|
+
task mermaid_erd: :environment do
|
12
|
+
result = {
|
13
|
+
Models: [],
|
14
|
+
Relations: []
|
15
|
+
}
|
16
|
+
|
17
|
+
::Rails.application.eager_load!
|
18
|
+
::ActiveRecord::Base.descendants.each do |defined_model|
|
19
|
+
next unless defined_model.table_exists?
|
20
|
+
model = {
|
21
|
+
TableName: defined_model.table_name,
|
22
|
+
ModelName: defined_model.name,
|
23
|
+
IsModelExist: true,
|
24
|
+
Columns: []
|
25
|
+
}
|
26
|
+
|
27
|
+
next if defined_model.table_name.blank?
|
28
|
+
|
29
|
+
foreign_keys = ::ActiveRecord::Schema.foreign_keys(defined_model.table_name).map { |k| k.options[:column] }
|
30
|
+
primary_key = defined_model.primary_key
|
31
|
+
|
32
|
+
defined_model.columns.each do |column|
|
33
|
+
key = ""
|
34
|
+
if column.name == primary_key
|
35
|
+
key = "PK"
|
36
|
+
elsif foreign_keys.include?(column.name)
|
37
|
+
key = "FK"
|
38
|
+
end
|
39
|
+
model[:Columns] << {
|
40
|
+
name: column.name,
|
41
|
+
type: column.type,
|
42
|
+
key: key,
|
43
|
+
comment: column.comment
|
44
|
+
}
|
45
|
+
end
|
46
|
+
|
47
|
+
result[:Models] << model
|
48
|
+
|
49
|
+
next unless model[:IsModelExist]
|
50
|
+
|
51
|
+
defined_model.reflect_on_all_associations(:has_many).each do |h|
|
52
|
+
if h.options[:through]
|
53
|
+
next
|
54
|
+
end
|
55
|
+
if h.options[:class_name]
|
56
|
+
reverse_relation = result[:Relations].find { |r| r[:RightModelName] == model[:ModelName] && r[:LeftModelName] == h.options[:class_name] }
|
57
|
+
if reverse_relation
|
58
|
+
reverse_relation[:Comment] = if reverse_relation[:Comment] == ""
|
59
|
+
"has_many #{h.name}"
|
60
|
+
else
|
61
|
+
"#{reverse_relation[:Comment]} : has_many #{h.name}"
|
62
|
+
end
|
63
|
+
else
|
64
|
+
result[:Relations] << {
|
65
|
+
LeftModelName: model[:ModelName],
|
66
|
+
LeftValue: "||",
|
67
|
+
RightModelName: h.name.to_s.classify,
|
68
|
+
RightValue: "o{",
|
69
|
+
Comment: "has_many #{h.name}"
|
70
|
+
}
|
71
|
+
end
|
72
|
+
else
|
73
|
+
reverse_relation = result[:Relations].find { |r| r[:RightModelName] == model[:ModelName] && r[:LeftModelName] == h.name.to_s.classify }
|
74
|
+
if reverse_relation
|
75
|
+
else
|
76
|
+
result[:Relations] << {
|
77
|
+
LeftModelName: model[:ModelName],
|
78
|
+
LeftValue: "||",
|
79
|
+
RightModelName: h.name.to_s.classify,
|
80
|
+
RightValue: "o{",
|
81
|
+
Comment: ""
|
82
|
+
}
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
|
87
|
+
defined_model.reflect_on_all_associations(:belongs_to).each do |h|
|
88
|
+
if h.options[:class_name]
|
89
|
+
reverse_relation = result[:Relations].find { |r| r[:RightModelName] == model[:ModelName] && r[:LeftModelName] == h.options[:class_name] }
|
90
|
+
|
91
|
+
if reverse_relation
|
92
|
+
if (::Rails.application.config.active_record.belongs_to_required_by_default && h.options[:optional]) || (!::Rails.application.config.active_record.belongs_to_required_by_default && !h.options[:requried])
|
93
|
+
reverse_relation[:LeftValue] = "|o"
|
94
|
+
end
|
95
|
+
reverse_relation[:Comment] = if reverse_relation[:Comment] == ""
|
96
|
+
"belongs_to #{h.name}"
|
97
|
+
else
|
98
|
+
"#{reverse_relation[:Comment]} : belongs_to #{h.name}"
|
99
|
+
end
|
100
|
+
else
|
101
|
+
result[:Relations] << if (::Rails.application.config.active_record.belongs_to_required_by_default && h.options[:optional]) || (!::Rails.application.config.active_record.belongs_to_required_by_default && !h.options[:requried])
|
102
|
+
{
|
103
|
+
LeftModelName: model[:ModelName],
|
104
|
+
LeftValue: "}o",
|
105
|
+
RightModelName: h.options[:class_name],
|
106
|
+
RightValue: "o|",
|
107
|
+
Comment: "belongs_to #{h.name}"
|
108
|
+
}
|
109
|
+
else
|
110
|
+
{
|
111
|
+
LeftModelName: model[:ModelName],
|
112
|
+
LeftValue: "}o",
|
113
|
+
RightModelName: h.options[:class_name],
|
114
|
+
RightValue: "||",
|
115
|
+
Comment: "belongs_to #{h.name}"
|
116
|
+
}
|
117
|
+
end
|
118
|
+
end
|
119
|
+
else
|
120
|
+
reverse_relation = result[:Relations].find { |r| r[:RightModelName] == model[:ModelName] && r[:LeftModelName] == h.name.to_s.classify }
|
121
|
+
|
122
|
+
if reverse_relation
|
123
|
+
if (::Rails.application.config.active_record.belongs_to_required_by_default && h.options[:optional]) || (!::Rails.application.config.active_record.belongs_to_required_by_default && !h.options[:requried])
|
124
|
+
reverse_relation[:LeftValue] = "|o"
|
125
|
+
end
|
126
|
+
else
|
127
|
+
result[:Relations] << if (::Rails.application.config.active_record.belongs_to_required_by_default && h.options[:optional]) || (!::Rails.application.config.active_record.belongs_to_required_by_default && !h.options[:requried])
|
128
|
+
{
|
129
|
+
LeftModelName: model[:ModelName],
|
130
|
+
LeftValue: "}o",
|
131
|
+
RightModelName: h.name.to_s.classify,
|
132
|
+
RightValue: "o|",
|
133
|
+
Comment: ""
|
134
|
+
}
|
135
|
+
else
|
136
|
+
{
|
137
|
+
LeftModelName: model[:ModelName],
|
138
|
+
LeftValue: "}o",
|
139
|
+
RightModelName: h.name.to_s.classify,
|
140
|
+
RightValue: "||",
|
141
|
+
Comment: ""
|
142
|
+
}
|
143
|
+
end
|
144
|
+
end
|
145
|
+
end
|
146
|
+
end
|
147
|
+
|
148
|
+
defined_model.reflect_on_all_associations(:has_one).each do |h|
|
149
|
+
if h.options[:class_name]
|
150
|
+
reverse_relation = result[:Relations].find { |r| r[:RightModelName] == model[:ModelName] && r[:LeftModelName] == h.options[:class_name] }
|
151
|
+
if reverse_relation
|
152
|
+
reverse_relation[:LeftValue] = "|o"
|
153
|
+
reverse_relation[:Comment] = if reverse_relation[:Comment] == ""
|
154
|
+
"has_one #{h.name}"
|
155
|
+
else
|
156
|
+
"#{reverse_relation[:Comment]} : has_one #{h.name}"
|
157
|
+
end
|
158
|
+
|
159
|
+
if h.options[:through]
|
160
|
+
next
|
161
|
+
end
|
162
|
+
else
|
163
|
+
result[:Relations] << {
|
164
|
+
LeftModelName: model[:ModelName],
|
165
|
+
LeftValue: "||",
|
166
|
+
RightModelName: h.options[:class_name],
|
167
|
+
RightValue: "o|",
|
168
|
+
Comment: "has_one #{h.name}"
|
169
|
+
}
|
170
|
+
end
|
171
|
+
else
|
172
|
+
reverse_relation = result[:Relations].find { |r| r[:RightModelName] == model[:ModelName] && r[:LeftModelName] == h.name.to_s.classify }
|
173
|
+
if reverse_relation
|
174
|
+
reverse_relation[:LeftValue] = "|o"
|
175
|
+
if h.options[:through]
|
176
|
+
next
|
177
|
+
end
|
178
|
+
else
|
179
|
+
result[:Relations] << {
|
180
|
+
LeftModelName: model[:ModelName],
|
181
|
+
LeftValue: "||",
|
182
|
+
RightModelName: h.name.to_s.classify,
|
183
|
+
RightValue: "o|",
|
184
|
+
Comment: "has_one #{h.name}"
|
185
|
+
}
|
186
|
+
end
|
187
|
+
end
|
188
|
+
end
|
189
|
+
end
|
190
|
+
|
191
|
+
version = VERSION
|
192
|
+
app_name = ::Rails.application.class.try(:parent_name) || ::Rails.application.class.try(:module_parent_name)
|
193
|
+
logo = File.read(File.expand_path("./assets/logo.svg", __dir__))
|
194
|
+
erb = ERB.new(File.read(File.expand_path("./templates/index.html.erb", __dir__)))
|
195
|
+
result_html = erb.result(binding)
|
196
|
+
|
197
|
+
result_dir = Rails.root.join("./mermaid_erd")
|
198
|
+
result_file = result_dir.join("./index.html")
|
199
|
+
FileUtils.mkdir_p(result_dir)
|
200
|
+
File.write(result_file, result_html)
|
201
|
+
end
|
202
|
+
end
|
@@ -0,0 +1,584 @@
|
|
1
|
+
<!DOCTYPE html>
|
2
|
+
<html lang="en">
|
3
|
+
<head>
|
4
|
+
<meta charset="UTF-8">
|
5
|
+
<title><%= app_name %> - Rails Mermaid ERD</title>
|
6
|
+
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
7
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
8
|
+
<link rel="icon" href="data:image/svg+xml;base64,<%= Base64.encode64(logo) %>">
|
9
|
+
<style>
|
10
|
+
#mermaid-erd {
|
11
|
+
fill: #9c9c9c !important;
|
12
|
+
}
|
13
|
+
</style>
|
14
|
+
</head>
|
15
|
+
<body>
|
16
|
+
<div id="app">
|
17
|
+
<header class="bg-red-600">
|
18
|
+
<nav class="mx-auto px-4">
|
19
|
+
<div class="flex w-full items-center justify-between py-4">
|
20
|
+
<h1 class="text-white inline-flex space-x-1 items-center">
|
21
|
+
<%= logo %>
|
22
|
+
<span class="font-bold">Rails Mermaid ERD</span>
|
23
|
+
</h1>
|
24
|
+
<div class="space-x-4 inline-flex items-center">
|
25
|
+
<select v-model="language" @change="setLanguage(language)" class="text-xs border-0 rounded py-1 pl-3 pr-8 focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-red-600">
|
26
|
+
<option value="en">English</option>
|
27
|
+
<option value="ja">Japanese - 日本語</option>
|
28
|
+
</select>
|
29
|
+
<a href="https://github.com/koedame/rails-mermaid_erd" class="text-white hover:text-red-200" target="_blank" rel="noopener noreferrer">
|
30
|
+
<svg class="h-6 w-6" fill="currentColor" viewBox="0 0 24 24">
|
31
|
+
<path
|
32
|
+
fillRule="evenodd"
|
33
|
+
d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"
|
34
|
+
clipRule="evenodd"
|
35
|
+
/>
|
36
|
+
</svg>
|
37
|
+
</a>
|
38
|
+
</div>
|
39
|
+
</div>
|
40
|
+
</nav>
|
41
|
+
</header>
|
42
|
+
|
43
|
+
<div class="flex">
|
44
|
+
<div class="w-[250px] border-r border-gray-300 h-full flex-none p-2">
|
45
|
+
<div class="p-2">
|
46
|
+
<h2 class="font-medium text-gray-900 mb-1">{{i18n[language]["actions"]["title"]}}</h2>
|
47
|
+
|
48
|
+
<div class="space-y-2">
|
49
|
+
<button @click="reset" class="text-xs border border-gray-300 rounded py-1 w-full hover:bg-gray-100 flex justify-center items-center focus:ring focus:ring-red-600 focus:ring-offset-2">
|
50
|
+
<svg class="w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
51
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M9 15L3 9m0 0l6-6M3 9h12a6 6 0 010 12h-3" />
|
52
|
+
</svg>
|
53
|
+
<span>{{i18n[language]["actions"]["reset"]}}</span>
|
54
|
+
</button>
|
55
|
+
|
56
|
+
<button @click="onCopyUrl" class="text-xs border border-gray-300 rounded py-1 w-full hover:bg-gray-100 flex justify-center items-center focus:ring focus:ring-red-600 focus:ring-offset-2">
|
57
|
+
<template v-if="isCopiedUrl">
|
58
|
+
<svg class="w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
59
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
60
|
+
</svg>
|
61
|
+
<span>{{i18n[language]["actions"]["copied_url"]}}</span>
|
62
|
+
</template>
|
63
|
+
<template v-else>
|
64
|
+
<svg class="w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
65
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25zM6.75 12h.008v.008H6.75V12zm0 3h.008v.008H6.75V15zm0 3h.008v.008H6.75V18z" />
|
66
|
+
</svg>
|
67
|
+
<span>{{i18n[language]["actions"]["copy_url"]}}</span>
|
68
|
+
</template>
|
69
|
+
</button>
|
70
|
+
|
71
|
+
<button @click="onCopyMermaid" class="text-xs border border-gray-300 rounded py-1 w-full hover:bg-gray-100 flex justify-center items-center focus:ring focus:ring-red-600 focus:ring-offset-2">
|
72
|
+
<template v-if="isCopiedMermaid">
|
73
|
+
<svg class="w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
74
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
75
|
+
</svg>
|
76
|
+
<span>{{i18n[language]["actions"]["copied_mermaid_code"]}}</span>
|
77
|
+
</template>
|
78
|
+
<template v-else>
|
79
|
+
<svg class="w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
80
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25zM6.75 12h.008v.008H6.75V12zm0 3h.008v.008H6.75V15zm0 3h.008v.008H6.75V18z" />
|
81
|
+
</svg>
|
82
|
+
<span>{{i18n[language]["actions"]["copy_mermaid_code"]}}</span>
|
83
|
+
</template>
|
84
|
+
</button>
|
85
|
+
|
86
|
+
<button @click="onCopyMarkdown" class="text-xs border border-gray-300 rounded py-1 w-full hover:bg-gray-100 flex justify-center items-center focus:ring focus:ring-red-600 focus:ring-offset-2">
|
87
|
+
<template v-if="isCopiedMarkdown">
|
88
|
+
<svg class="w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
89
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5" />
|
90
|
+
</svg>
|
91
|
+
<span>{{i18n[language]["actions"]["copied_markdown_code"]}}</span>
|
92
|
+
</template>
|
93
|
+
<template v-else>
|
94
|
+
<svg class="w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
95
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M9 12h3.75M9 15h3.75M9 18h3.75m3 .75H18a2.25 2.25 0 002.25-2.25V6.108c0-1.135-.845-2.098-1.976-2.192a48.424 48.424 0 00-1.123-.08m-5.801 0c-.065.21-.1.433-.1.664 0 .414.336.75.75.75h4.5a.75.75 0 00.75-.75 2.25 2.25 0 00-.1-.664m-5.8 0A2.251 2.251 0 0113.5 2.25H15c1.012 0 1.867.668 2.15 1.586m-5.8 0c-.376.023-.75.05-1.124.08C9.095 4.01 8.25 4.973 8.25 6.108V8.25m0 0H4.875c-.621 0-1.125.504-1.125 1.125v11.25c0 .621.504 1.125 1.125 1.125h9.75c.621 0 1.125-.504 1.125-1.125V9.375c0-.621-.504-1.125-1.125-1.125H8.25zM6.75 12h.008v.008H6.75V12zm0 3h.008v.008H6.75V15zm0 3h.008v.008H6.75V18z" />
|
96
|
+
</svg>
|
97
|
+
<span>{{i18n[language]["actions"]["copy_markdown_code"]}}</span>
|
98
|
+
</template>
|
99
|
+
</button>
|
100
|
+
|
101
|
+
<button @click="onDownloadSvg" class="text-xs border border-gray-300 rounded py-1 w-full hover:bg-gray-100 flex justify-center items-center focus:ring focus:ring-red-600 focus:ring-offset-2">
|
102
|
+
<svg class="w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
103
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
104
|
+
</svg>
|
105
|
+
<span>{{i18n[language]["actions"]["download_svg_file"]}}</span>
|
106
|
+
</button>
|
107
|
+
|
108
|
+
<button @click="onDownloadPng" class="text-xs border border-gray-300 rounded py-1 w-full hover:bg-gray-100 flex justify-center items-center focus:ring focus:ring-red-600 focus:ring-offset-2">
|
109
|
+
<svg class="w-4 h-4 mr-2" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
110
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M3 16.5v2.25A2.25 2.25 0 005.25 21h13.5A2.25 2.25 0 0021 18.75V16.5M16.5 12L12 16.5m0 0L7.5 12m4.5 4.5V3" />
|
111
|
+
</svg>
|
112
|
+
<span>{{i18n[language]["actions"]["download_png_file"]}}</span>
|
113
|
+
</button>
|
114
|
+
</div>
|
115
|
+
</div>
|
116
|
+
|
117
|
+
<div class="p-2">
|
118
|
+
<h2 class="font-medium text-gray-900 mb-1">{{i18n[language]["options"]["title"]}}</h2>
|
119
|
+
|
120
|
+
<div class="space-y-1">
|
121
|
+
<div class="min-w-0 flex-1 text-sm">
|
122
|
+
<label class="relative flex items-start cursor-pointer hover:bg-gray-100 rounded">
|
123
|
+
<input
|
124
|
+
type="checkbox"
|
125
|
+
class="h-4 w-4 rounded border-gray-300 text-red-600 mr-2 focus:ring-red-600"
|
126
|
+
v-model="isPreviewRelations"
|
127
|
+
@change="updateHash"
|
128
|
+
/>
|
129
|
+
<span class="text-xs text-gray-900">{{i18n[language]["options"]["preview_relationships"]}}</span>
|
130
|
+
</label>
|
131
|
+
</div>
|
132
|
+
|
133
|
+
<div class="min-w-0 flex-1 text-sm">
|
134
|
+
<label class="relative flex items-start cursor-pointer hover:bg-gray-100 rounded">
|
135
|
+
<input
|
136
|
+
type="checkbox"
|
137
|
+
class="h-4 w-4 rounded border-gray-300 text-red-600 mr-2 focus:ring-red-600"
|
138
|
+
v-model="isHideColumns"
|
139
|
+
@change="updateHash"
|
140
|
+
/>
|
141
|
+
<span class="text-xs text-gray-900">{{i18n[language]["options"]["hide_columns"]}}</span>
|
142
|
+
</label>
|
143
|
+
</div>
|
144
|
+
|
145
|
+
<div class="min-w-0 flex-1 text-sm">
|
146
|
+
<label :class="`relative flex items-start hover:bg-gray-100 rounded ${ isHideColumns ? 'cursor-not-allowed' : 'cursor-pointer' }`">
|
147
|
+
<input
|
148
|
+
type="checkbox"
|
149
|
+
:class="`h-4 w-4 rounded border-gray-300 mr-2 focus:ring-red-600 ${ isHideColumns ? 'text-gray-400' : 'text-red-600'}`"
|
150
|
+
v-model="isShowKey"
|
151
|
+
@change="updateHash"
|
152
|
+
:disabled="isHideColumns"
|
153
|
+
/>
|
154
|
+
<span :class="`text-xs ${ isHideColumns ? 'text-gray-400' : 'text-gray-900'}`">{{i18n[language]["options"]["show_key"]}}</span>
|
155
|
+
</label>
|
156
|
+
</div>
|
157
|
+
|
158
|
+
<div class="min-w-0 flex-1 text-sm">
|
159
|
+
<label :class="`relative flex items-start hover:bg-gray-100 rounded ${ isHideColumns ? 'cursor-not-allowed' : 'cursor-pointer' }`">
|
160
|
+
<input
|
161
|
+
type="checkbox"
|
162
|
+
:class="`h-4 w-4 rounded border-gray-300 mr-2 focus:ring-red-600 ${ isHideColumns ? 'text-gray-400' : 'text-red-600'}`"
|
163
|
+
v-model="isShowComment"
|
164
|
+
@change="updateHash"
|
165
|
+
:disabled="isHideColumns"
|
166
|
+
/>
|
167
|
+
<span :class="`text-xs ${ isHideColumns ? 'text-gray-400' : 'text-gray-900'}`">{{i18n[language]["options"]["show_column_comment"]}}</span>
|
168
|
+
</label>
|
169
|
+
</div>
|
170
|
+
</div>
|
171
|
+
</div>
|
172
|
+
|
173
|
+
<div class="p-2">
|
174
|
+
<h2 class="font-medium text-gray-900 mb-1">{{i18n[language]["models"]["title"]}}</h2>
|
175
|
+
|
176
|
+
<div>
|
177
|
+
<input type="search" class="rounded text-xs w-full py-1 px-2 border border-gray-300 bg-gray-100 focus:border-red-600 focus:ring-red-600" :placeholder="i18n[language]['models']['filter']" v-model="filterText">
|
178
|
+
</div>
|
179
|
+
|
180
|
+
<div class="space-x-1 text-xs text-gray-900 my-1">
|
181
|
+
<span>{{i18n[language]["models"]["select"]}}</span>
|
182
|
+
<button @click="onSelectAll" class="text-red-600 font-bold hover:text-red-900 rounded focus:ring-2 focus:ring-red-600 focus:ring-offset-1">{{i18n[language]["models"]["all"]}}</button>
|
183
|
+
<span>/</span>
|
184
|
+
<button @click="onUnselectAll" class="text-red-600 font-bold hover:text-red-900 rounded focus:ring-2 focus:ring-red-600 focus:ring-offset-1">{{i18n[language]["models"]["none"]}}</button>
|
185
|
+
</div>
|
186
|
+
|
187
|
+
<div class="space-y-1">
|
188
|
+
<div class="inline-flex items-center text-xs text-gray-700" v-if="isExistsNoModelTable">
|
189
|
+
<svg class="text-orange-900 mr-1 h-3 w-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
190
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
|
191
|
+
</svg>
|
192
|
+
<span>{{i18n[language]["models"]["no_model_file"]}}</span>
|
193
|
+
</div>
|
194
|
+
|
195
|
+
<label class="relative flex items-start cursor-pointer hover:bg-gray-100 rounded" v-for="model in filteredModelList">
|
196
|
+
<div class="min-w-0 flex-1 text-xs text-gray-900 inline-flex items-center">
|
197
|
+
<input
|
198
|
+
type="checkbox"
|
199
|
+
class="h-4 w-4 rounded border-gray-300 text-red-600 mr-2 focus:ring-red-600"
|
200
|
+
:value="model.ModelName"
|
201
|
+
:checked="selectModels.includes(model.ModelName)"
|
202
|
+
@change="onChange"
|
203
|
+
/>
|
204
|
+
<svg v-if="!model.IsModelExist" class="text-orange-900 mr-1 h-3 w-3" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24" stroke-width="1.5" stroke="currentColor" class="w-6 h-6">
|
205
|
+
<path stroke-linecap="round" stroke-linejoin="round" d="M12 9v3.75m-9.303 3.376c-.866 1.5.217 3.374 1.948 3.374h14.71c1.73 0 2.813-1.874 1.948-3.374L13.949 3.378c-.866-1.5-3.032-1.5-3.898 0L2.697 16.126zM12 15.75h.007v.008H12v-.008z" />
|
206
|
+
</svg>
|
207
|
+
<span>{{model.ModelName}}</span>
|
208
|
+
</div>
|
209
|
+
</label>
|
210
|
+
</div>
|
211
|
+
</div>
|
212
|
+
</div>
|
213
|
+
|
214
|
+
<div class="flex-1 bg-gray-900">
|
215
|
+
<div class="space-x-2 inline-flex p-4">
|
216
|
+
<button @click="tab = 'erd'" :class="`text-xs py-1 px-2 rounded hover:bg-white focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-900 ${tab === 'erd' ? 'bg-white text-gray-900' : 'bg-gray-400 text-gray-900'}`">{{i18n[language]["tab"]["erd"]}}</button>
|
217
|
+
<button @click="tab = 'code'" :class="`text-xs py-1 px-2 rounded hover:bg-white focus:ring-2 focus:ring-white focus:ring-offset-2 focus:ring-offset-gray-900 ${tab === 'code' ? 'bg-white text-gray-900' : 'bg-gray-400 text-gray-900'}`">{{i18n[language]["tab"]["code"]}}</button>
|
218
|
+
</div>
|
219
|
+
<div v-show="tab === 'erd'" class="px-4 w-full min-h-[calc(100vh-56px-32px-56px)]" id="preview"></div>
|
220
|
+
<textarea v-show="tab === 'code'" class="px-4 bg-gray-900 text-gray-300 font-mono w-full text-xs min-h-[calc(100vh-56px-32px-56px)] border-0 focus:ring-0" readonly v-model="mermaidErd"></textarea>
|
221
|
+
</div>
|
222
|
+
</div>
|
223
|
+
|
224
|
+
<footer class="bg-gray-100">
|
225
|
+
<p class="text-center text-xs text-gray-600 py-2">
|
226
|
+
<a href="https://github.com/koedame/rails-mermaid_erd" class="hover:text-gray-400" target="_blank" rel="noopener noreferrer">
|
227
|
+
Rails Mermaid ERD v<%= version %>
|
228
|
+
</a>
|
229
|
+
</p>
|
230
|
+
</footer>
|
231
|
+
</div>
|
232
|
+
|
233
|
+
<script src="https://cdn.tailwindcss.com/3.1.8?plugins=forms@0.5.2,typography@0.5.4"></script>
|
234
|
+
<script src="https://cdnjs.cloudflare.com/ajax/libs/mermaid/9.1.7/mermaid.min.js"></script>
|
235
|
+
<script src="https://unpkg.com/vue@3.2.40/dist/vue.global.js"></script>
|
236
|
+
|
237
|
+
<script>window.SCHEMA_DATA=<%= result.to_json %></script>
|
238
|
+
<script>
|
239
|
+
window.i18n = {
|
240
|
+
en: {
|
241
|
+
actions: {
|
242
|
+
title: 'Actions',
|
243
|
+
reset: 'Reset',
|
244
|
+
copy_mermaid_code: 'Copy Mermaid Code',
|
245
|
+
copied_mermaid_code: 'Copied Mermaid Code',
|
246
|
+
copy_markdown_code: 'Copy Markdown Code',
|
247
|
+
copied_markdown_code: 'Copied Markdown Code',
|
248
|
+
copy_url: 'Copy Link for Sharing',
|
249
|
+
copied_url: 'Copied Link for Sharing',
|
250
|
+
download_svg_file: 'Download SVG File',
|
251
|
+
download_png_file: 'Download PNG File',
|
252
|
+
},
|
253
|
+
options: {
|
254
|
+
title: 'Options',
|
255
|
+
preview_relationships: 'Preview Relationships',
|
256
|
+
show_key: 'Show Key',
|
257
|
+
show_column_comment: 'Show Column Comment',
|
258
|
+
hide_columns: 'Hide Columns',
|
259
|
+
},
|
260
|
+
models: {
|
261
|
+
title: 'Models',
|
262
|
+
select: 'Select',
|
263
|
+
all: 'All',
|
264
|
+
none: 'None',
|
265
|
+
filter: 'Filter',
|
266
|
+
no_model_file: 'No Model File',
|
267
|
+
},
|
268
|
+
tab: {
|
269
|
+
erd: 'ERD',
|
270
|
+
code: 'Code',
|
271
|
+
},
|
272
|
+
},
|
273
|
+
ja: {
|
274
|
+
actions: {
|
275
|
+
title: '操作',
|
276
|
+
reset: 'リセット',
|
277
|
+
copy_mermaid_code: 'Mermaid コードをコピー',
|
278
|
+
copied_mermaid_code: 'コピーしました',
|
279
|
+
copy_markdown_code: 'Markdown コードをコピー',
|
280
|
+
copied_markdown_code: 'コピーしました',
|
281
|
+
copy_url: '共有リンクをコピー',
|
282
|
+
copied_url: 'コピーしました',
|
283
|
+
download_svg_file: 'SVG ファイルをダウンロード',
|
284
|
+
download_png_file: 'PNG ファイルをダウンロード',
|
285
|
+
},
|
286
|
+
options: {
|
287
|
+
title: '設定',
|
288
|
+
preview_relationships: 'リレーションをプレビュー',
|
289
|
+
show_key: 'キーを表示',
|
290
|
+
show_column_comment: 'コメントを表示',
|
291
|
+
hide_columns: 'カラムを非表示',
|
292
|
+
},
|
293
|
+
models: {
|
294
|
+
title: 'モデル',
|
295
|
+
select: '選択',
|
296
|
+
all: '全て',
|
297
|
+
none: 'なし',
|
298
|
+
filter: '絞り込み',
|
299
|
+
no_model_file: 'モデル未定義',
|
300
|
+
},
|
301
|
+
tab: {
|
302
|
+
erd: 'ER図',
|
303
|
+
code: 'コード',
|
304
|
+
},
|
305
|
+
}
|
306
|
+
}
|
307
|
+
</script>
|
308
|
+
<script>
|
309
|
+
const App = {
|
310
|
+
setup(){
|
311
|
+
const language = Vue.ref('en')
|
312
|
+
const isCopiedMermaid = Vue.ref(false)
|
313
|
+
const isCopiedMarkdown = Vue.ref(false)
|
314
|
+
const tab = Vue.ref('erd')
|
315
|
+
|
316
|
+
const schemaData = {
|
317
|
+
...window.SCHEMA_DATA,
|
318
|
+
Models: window.SCHEMA_DATA.Models.sort((a, b) => a.ModelName < b.ModelName ? -1 : 1)
|
319
|
+
}
|
320
|
+
const selectModels = Vue.ref([])
|
321
|
+
const isPreviewRelations = Vue.ref(false)
|
322
|
+
const isShowKey = Vue.ref(false)
|
323
|
+
const isShowComment = Vue.ref(false)
|
324
|
+
const isHideColumns = Vue.ref(false)
|
325
|
+
|
326
|
+
const restoreFromHash = () => {
|
327
|
+
try {
|
328
|
+
const h = location.hash.substr(1, location.hash.length)
|
329
|
+
const parsedData = JSON.parse(atob(h))
|
330
|
+
selectModels.value = parsedData.selectModels || []
|
331
|
+
isPreviewRelations.value = !!parsedData.isPreviewRelations
|
332
|
+
isShowKey.value = !!parsedData.isShowKey
|
333
|
+
isShowComment.value = !!parsedData.isShowComment
|
334
|
+
isHideColumns.value = !!parsedData.isHideColumns
|
335
|
+
} catch {
|
336
|
+
reset()
|
337
|
+
}
|
338
|
+
}
|
339
|
+
|
340
|
+
const reset = () => {
|
341
|
+
selectModels.value = []
|
342
|
+
schemaData.Models.forEach((model) => {
|
343
|
+
selectModels.value.push(model.ModelName)
|
344
|
+
})
|
345
|
+
isPreviewRelations.value = false
|
346
|
+
isShowKey.value = false
|
347
|
+
isShowComment.value = false
|
348
|
+
isHideColumns.value = false
|
349
|
+
updateHash()
|
350
|
+
}
|
351
|
+
|
352
|
+
const updateHash = () => {
|
353
|
+
location.hash = btoa(JSON.stringify({
|
354
|
+
selectModels: selectModels.value,
|
355
|
+
isPreviewRelations: isPreviewRelations.value,
|
356
|
+
isShowKey: isShowKey.value,
|
357
|
+
isShowComment: isShowComment.value,
|
358
|
+
isHideColumns: isHideColumns.value,
|
359
|
+
}))
|
360
|
+
}
|
361
|
+
|
362
|
+
const onChange = (e) => {
|
363
|
+
if (e.target.checked) {
|
364
|
+
selectModels.value.push(e.target.value)
|
365
|
+
} else {
|
366
|
+
selectModels.value = selectModels.value.filter((selectModel) => selectModel !== e.target.value)
|
367
|
+
}
|
368
|
+
selectModels.value
|
369
|
+
updateHash()
|
370
|
+
}
|
371
|
+
|
372
|
+
const onSelectAll = () => {
|
373
|
+
filteredModelList.value.forEach((model) => {
|
374
|
+
if (!selectModels.value.includes(model.ModelName)) {
|
375
|
+
selectModels.value.push(model.ModelName)
|
376
|
+
}
|
377
|
+
})
|
378
|
+
updateHash()
|
379
|
+
}
|
380
|
+
|
381
|
+
const onUnselectAll = () => {
|
382
|
+
selectModels.value = selectModels.value.filter((model) => {
|
383
|
+
return !filteredModelList.value.some((fm) => fm.ModelName === model)
|
384
|
+
})
|
385
|
+
updateHash()
|
386
|
+
}
|
387
|
+
|
388
|
+
isCopiedUrl = Vue.ref(false)
|
389
|
+
const onCopyUrl = () => {
|
390
|
+
navigator.clipboard.writeText(location.href);
|
391
|
+
isCopiedUrl.value = true
|
392
|
+
|
393
|
+
setTimeout(() => {
|
394
|
+
if (isCopiedUrl) {
|
395
|
+
isCopiedUrl.value = false
|
396
|
+
}
|
397
|
+
}, 1000)
|
398
|
+
}
|
399
|
+
|
400
|
+
const mermaidErd = Vue.computed(() => {
|
401
|
+
const lines = []
|
402
|
+
lines.push('erDiagram')
|
403
|
+
lines.push(' %% --------------------------------------------------------')
|
404
|
+
lines.push(' %% Generated by "Rails Mermaid ERD"')
|
405
|
+
lines.push(' %% https://github.com/koedame/rails-mermaid_erd')
|
406
|
+
lines.push(` %% Restore Hash: ${location.hash}`)
|
407
|
+
lines.push(' %% --------------------------------------------------------')
|
408
|
+
lines.push('')
|
409
|
+
|
410
|
+
filteredData.value.Models.forEach(model => {
|
411
|
+
lines.push(` %% table name: ${model.TableName}`)
|
412
|
+
lines.push(` ${model.ModelName.replace(/:/g, '-')} {`)
|
413
|
+
|
414
|
+
if (!isHideColumns.value) {
|
415
|
+
model.Columns.forEach(column => {
|
416
|
+
lines.push(` ${column.type} ${column.name} ${isShowKey.value ? column.key : ''} ${isShowComment.value ? `"${column.comment || ''}"` : ''}`)
|
417
|
+
})
|
418
|
+
}
|
419
|
+
|
420
|
+
lines.push(` }`)
|
421
|
+
lines.push('')
|
422
|
+
});
|
423
|
+
|
424
|
+
filteredData.value.Relations.forEach(relation => {
|
425
|
+
lines.push(` ${relation.LeftModelName.replace(/:/g, '-')} ${relation.LeftValue}--${relation.RightValue} ${relation.RightModelName.replace(/:/g, '-')} : "${relation.Comment}"`)
|
426
|
+
});
|
427
|
+
|
428
|
+
return lines.join("\n")
|
429
|
+
})
|
430
|
+
|
431
|
+
const filteredData = Vue.computed(() => {
|
432
|
+
let relations
|
433
|
+
if (isPreviewRelations.value) {
|
434
|
+
relations = schemaData.Relations.filter(relation => {
|
435
|
+
return selectModels.value.includes(relation.LeftModelName) || selectModels.value.includes(relation.RightModelName)
|
436
|
+
})
|
437
|
+
} else {
|
438
|
+
relations = schemaData.Relations.filter(relation => {
|
439
|
+
return selectModels.value.includes(relation.LeftModelName) && selectModels.value.includes(relation.RightModelName)
|
440
|
+
})
|
441
|
+
}
|
442
|
+
|
443
|
+
return {
|
444
|
+
Models: schemaData.Models.filter(model => selectModels.value.includes(model.ModelName)),
|
445
|
+
Relations: relations
|
446
|
+
}
|
447
|
+
})
|
448
|
+
|
449
|
+
const graph = Vue.ref()
|
450
|
+
|
451
|
+
const filterText = Vue.ref('')
|
452
|
+
|
453
|
+
const reRender = () => {
|
454
|
+
mermaid.initialize({theme: 'dark'})
|
455
|
+
mermaid.init()
|
456
|
+
graph.value = mermaid.mermaidAPI.render("mermaid-erd", mermaidErd.value);
|
457
|
+
document.getElementById('preview').innerHTML = graph.value
|
458
|
+
}
|
459
|
+
|
460
|
+
const onCopyMermaid = () => {
|
461
|
+
navigator.clipboard.writeText(mermaidErd.value);
|
462
|
+
isCopiedMermaid.value = true
|
463
|
+
|
464
|
+
setTimeout(() => {
|
465
|
+
if (isCopiedMermaid) {
|
466
|
+
isCopiedMermaid.value = false
|
467
|
+
}
|
468
|
+
}, 1000)
|
469
|
+
}
|
470
|
+
|
471
|
+
const onCopyMarkdown = () => {
|
472
|
+
navigator.clipboard.writeText(`\`\`\`mermaid\n${mermaidErd.value}\n\`\`\`\n`);
|
473
|
+
isCopiedMarkdown.value = true
|
474
|
+
|
475
|
+
setTimeout(() => {
|
476
|
+
if (isCopiedMarkdown) {
|
477
|
+
isCopiedMarkdown.value = false
|
478
|
+
}
|
479
|
+
}, 1000)
|
480
|
+
}
|
481
|
+
|
482
|
+
const onDownloadSvg = () => {
|
483
|
+
const svgBlob = new Blob([graph.value], { type: 'image/svg+xml;charset=utf-8' });
|
484
|
+
const svgUrl = URL.createObjectURL(svgBlob);
|
485
|
+
|
486
|
+
const a = document.createElement("a");
|
487
|
+
a.href = svgUrl;
|
488
|
+
a.setAttribute("download", "mermaid_erd.svg");
|
489
|
+
a.dispatchEvent(new MouseEvent("click"));
|
490
|
+
|
491
|
+
URL.revokeObjectURL(svgUrl);
|
492
|
+
}
|
493
|
+
|
494
|
+
const onDownloadPng = () => {
|
495
|
+
const svg = document.querySelector("#preview > svg");
|
496
|
+
const svgData = new XMLSerializer().serializeToString(svg);
|
497
|
+
const canvas = document.createElement("canvas");
|
498
|
+
canvas.width = svg.width.baseVal.value;
|
499
|
+
canvas.height = svg.height.baseVal.value;
|
500
|
+
|
501
|
+
const ctx = canvas.getContext("2d");
|
502
|
+
const image = new Image;
|
503
|
+
image.onload = () => {
|
504
|
+
ctx.drawImage(image, 0, 0);
|
505
|
+
const a = document.createElement("a");
|
506
|
+
a.href = canvas.toDataURL("image/png");
|
507
|
+
a.setAttribute("download", "mermaid_erd.png");
|
508
|
+
a.dispatchEvent(new MouseEvent("click"));
|
509
|
+
}
|
510
|
+
image.src = "data:image/svg+xml;charset=utf-8;base64," + btoa(unescape(encodeURIComponent(svgData)));
|
511
|
+
}
|
512
|
+
|
513
|
+
const filteredModelList = Vue.computed(() => {
|
514
|
+
return schemaData.Models.filter((model) => {
|
515
|
+
if (model.ModelName.toLowerCase().includes(filterText.value.toLowerCase())) {
|
516
|
+
return true
|
517
|
+
}
|
518
|
+
if (model.TableName.toLowerCase().includes(filterText.value.toLowerCase())) {
|
519
|
+
return true
|
520
|
+
}
|
521
|
+
})
|
522
|
+
})
|
523
|
+
|
524
|
+
const isExistsNoModelTable = Vue.computed(() => {
|
525
|
+
return filteredModelList.value.some((model) => !model.IsModelExist)
|
526
|
+
})
|
527
|
+
|
528
|
+
const setLanguage = (l) => {
|
529
|
+
if (Object.keys(window.i18n).includes(l)) {
|
530
|
+
language.value = l
|
531
|
+
document.documentElement.lang = l
|
532
|
+
} else {
|
533
|
+
language.value = 'en'
|
534
|
+
document.documentElement.lang = 'en'
|
535
|
+
}
|
536
|
+
}
|
537
|
+
|
538
|
+
Vue.onMounted(() => {
|
539
|
+
setLanguage(window.navigator.language)
|
540
|
+
restoreFromHash()
|
541
|
+
reRender()
|
542
|
+
})
|
543
|
+
|
544
|
+
window.addEventListener('hashchange', () => {
|
545
|
+
restoreFromHash()
|
546
|
+
reRender()
|
547
|
+
}, false);
|
548
|
+
|
549
|
+
return {
|
550
|
+
filteredModelList,
|
551
|
+
filterText,
|
552
|
+
i18n: window.i18n,
|
553
|
+
isCopiedMarkdown,
|
554
|
+
isCopiedMermaid,
|
555
|
+
isCopiedUrl,
|
556
|
+
isExistsNoModelTable,
|
557
|
+
isPreviewRelations,
|
558
|
+
isShowComment,
|
559
|
+
isShowKey,
|
560
|
+
language,
|
561
|
+
mermaidErd,
|
562
|
+
onChange,
|
563
|
+
onCopyMarkdown,
|
564
|
+
onCopyMermaid,
|
565
|
+
onCopyUrl,
|
566
|
+
onDownloadPng,
|
567
|
+
onDownloadSvg,
|
568
|
+
onSelectAll,
|
569
|
+
onUnselectAll,
|
570
|
+
reset,
|
571
|
+
schemaData,
|
572
|
+
selectModels,
|
573
|
+
setLanguage,
|
574
|
+
tab,
|
575
|
+
updateHash,
|
576
|
+
isHideColumns,
|
577
|
+
}
|
578
|
+
}
|
579
|
+
}
|
580
|
+
|
581
|
+
Vue.createApp(App).mount('#app')
|
582
|
+
</script>
|
583
|
+
</body>
|
584
|
+
</html>
|
metadata
ADDED
@@ -0,0 +1,65 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: rails-mermaid_erd
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.1.1
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- 肥溜め
|
8
|
+
- "\U0001F4A9"
|
9
|
+
autorequire:
|
10
|
+
bindir: bin
|
11
|
+
cert_chain: []
|
12
|
+
date: 2022-10-08 00:00:00.000000000 Z
|
13
|
+
dependencies:
|
14
|
+
- !ruby/object:Gem::Dependency
|
15
|
+
name: activerecord
|
16
|
+
requirement: !ruby/object:Gem::Requirement
|
17
|
+
requirements:
|
18
|
+
- - ">="
|
19
|
+
- !ruby/object:Gem::Version
|
20
|
+
version: '4.2'
|
21
|
+
type: :runtime
|
22
|
+
prerelease: false
|
23
|
+
version_requirements: !ruby/object:Gem::Requirement
|
24
|
+
requirements:
|
25
|
+
- - ">="
|
26
|
+
- !ruby/object:Gem::Version
|
27
|
+
version: '4.2'
|
28
|
+
description:
|
29
|
+
email: []
|
30
|
+
executables: []
|
31
|
+
extensions: []
|
32
|
+
extra_rdoc_files: []
|
33
|
+
files:
|
34
|
+
- MIT-LICENSE
|
35
|
+
- README.md
|
36
|
+
- Rakefile
|
37
|
+
- lib/assets/logo.svg
|
38
|
+
- lib/rails-mermaid_erd.rb
|
39
|
+
- lib/rails-mermaid_erd/version.rb
|
40
|
+
- lib/templates/index.html.erb
|
41
|
+
homepage: https://github.com/koedame/rails-mermaid_erd
|
42
|
+
licenses:
|
43
|
+
- MIT
|
44
|
+
metadata:
|
45
|
+
homepage_uri: https://github.com/koedame/rails-mermaid_erd
|
46
|
+
post_install_message:
|
47
|
+
rdoc_options: []
|
48
|
+
require_paths:
|
49
|
+
- lib
|
50
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - ">="
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '0'
|
55
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
56
|
+
requirements:
|
57
|
+
- - ">="
|
58
|
+
- !ruby/object:Gem::Version
|
59
|
+
version: '0'
|
60
|
+
requirements: []
|
61
|
+
rubygems_version: 3.3.7
|
62
|
+
signing_key:
|
63
|
+
specification_version: 4
|
64
|
+
summary: Generate Mermaid ERD for Ruby on Rails
|
65
|
+
test_files: []
|