194 Commits
v0.1 ... v0.2

Author SHA1 Message Date
HeeseonYoon
1d16c03851 Update README.md 2022-11-11 14:15:17 +09:00
HeeseonYoon
7d30d398db Merge remote-tracking branch 'upstream/develop' 2022-11-11 14:13:43 +09:00
SA K
8dc60190b3 Merge pull request #288 from sakang07/develop
Fix
2022-11-11 14:09:29 +09:00
SA K
9d4bbc97ac [FE-data] Fix an issue where a database could not be selected when entering the Create Dataset menu in the navigation bar #287 2022-11-11 13:44:24 +09:00
HeeseonYoon
d938ca43ef Merge pull request #286 from yathoo88/develop_backend_yhs
[BE-widget] Add order by
2022-11-11 13:36:45 +09:00
HeeseonYoon
b6aa0c13b0 [BE-widget] Add order by 2022-11-11 13:35:29 +09:00
SA K
62066817c6 [FE-dashboard] Fix broken style of board and button 2022-11-11 11:45:48 +09:00
SA K
9ef9b698a1 Merge pull request #285 from sakang07/develop
Fix 404 error when clicking on main logo and clicking on dashboard post
2022-11-11 10:20:52 +09:00
SA K
d510a14947 [FE-router] Fix logo link to router link 2022-11-11 10:16:10 +09:00
SA K
1fb91c891c [FE-router] Fix 404 error when clicking on main logo and clicking on dashboard post #284 2022-11-11 10:07:53 +09:00
SA K
a16b845940 Merge pull request #283 from sakang07/develop
Modify router
2022-11-11 09:14:24 +09:00
SA K
4cbf9d0068 Merge remote-tracking branch 'upstream/develop' into develop 2022-11-11 00:39:21 +09:00
SA K
28cc8868fc [FE] Modify router 2022-11-11 00:34:38 +09:00
SA K
9466254b17 Merge pull request #282 from sakang07/develop
Modify the legend name differently for each aggregation method #281
2022-11-10 23:58:48 +09:00
SA K
5dd71d9985 [FE-chart] Modify the legend name differently for each aggregation method #281 2022-11-10 23:55:39 +09:00
HeeseonYoon
e42712fa46 Merge pull request #280 from yathoo88/develop_backend_yhs
Develop backend yhs
2022-11-10 23:24:11 +09:00
HeeseonYoon
f508d76ee6 Merge remote-tracking branch 'upstream/develop' into develop_backend_yhs 2022-11-10 23:23:08 +09:00
HeeseonYoon
23c01912ea [BE-test] Modify test files 2022-11-10 23:22:58 +09:00
Dosun Yun
5e3518bdb1 [BE-setting] Fix .gitignore rule 2022-11-10 20:50:42 +09:00
godyuo
7aadf008a2 Merge pull request #279 from godyuo/develop
test: update QTT-002, QTT-003 test script
2022-11-10 19:38:22 +09:00
손승우
095bb928a8 Merge remote-tracking branch 'origin/develop' into develop
# Conflicts:
#	backend-api/.gitignore
#	backend-api/test_connect_info.json
2022-11-10 19:34:24 +09:00
손승우
8799782a27 test: update QTT-002, QTT-003 test script 2022-11-10 19:33:06 +09:00
HeeseonYoon
ab58170d27 Merge pull request #278 from yathoo88/develop_backend_yhs
Develop backend yhs
2022-11-10 18:34:50 +09:00
HeeseonYoon
1e2f102eac Merge remote-tracking branch 'upstream/develop' into develop_backend_yhs 2022-11-10 18:33:48 +09:00
HeeseonYoon
1e24f9bea1 [BE-test] Modify test files 2022-11-10 18:33:24 +09:00
Dosun Yun
8c75c354d0 [FE-lint] Fix let to const 2022-11-10 18:25:37 +09:00
HeeseonYoon
a3b3a946cb Add gitignore 2022-11-10 18:11:47 +09:00
HeeseonYoon
ba3aaee726 Merge pull request #277 from yathoo88/develop_backend_yhs
Develop backend yhs
2022-11-10 17:49:55 +09:00
HeeseonYoon
5a52cc8eca Merge remote-tracking branch 'upstream/develop' into develop_backend_yhs 2022-11-10 17:48:57 +09:00
HeeseonYoon
74d6aba229 [FE-data] Modify eslint bug 2022-11-10 17:48:34 +09:00
SA K
d423106e27 Merge pull request #276 from sakang07/develop
Develop
2022-11-10 17:42:46 +09:00
SA K
d256d4cb1b Merge branch 'develop' of https://github.com/vanillabrain/vanillameta into develop 2022-11-10 17:33:38 +09:00
SA K
5c21a47557 [FE] Add error handling at 403 in apiHelper 2022-11-10 17:32:46 +09:00
HeeseonYoon
6fd55a640f Merge pull request #274 from yathoo88/develop_backend_yhs
Develop backend yhs
2022-11-10 17:30:55 +09:00
HeeseonYoon
ee05ebfb33 . 2022-11-10 17:30:18 +09:00
HeeseonYoon
9fd5cdcf8f Merge remote-tracking branch 'upstream/develop' into develop_backend_yhs 2022-11-10 17:26:47 +09:00
HeeseonYoon
95c56caa16 [FE-test] Modify aggregate function test file (QTT-004) 2022-11-10 17:26:33 +09:00
SA K
b47cf4f429 Merge pull request #273 from sakang07/develop
Fix bugs
2022-11-10 16:46:37 +09:00
SA K
338deb2807 Merge branch 'develop' of https://github.com/vanillabrain/vanillameta into develop 2022-11-10 16:43:13 +09:00
SA K
d6466d9a73 [FE] Fix an issue where a blank page appears when pressing logo #272 2022-11-10 16:43:00 +09:00
Dosun Yun
0a6395460f [BE-docker] add expose 2022-11-10 16:18:42 +09:00
godyuo
db4fb4f1a4 Merge pull request #271 from godyuo/develop
feat: add QTT-002, QTT-003 test script
2022-11-10 15:28:21 +09:00
손승우
f0f81d7229 feat: add QTT-002, QTT-003 test script 2022-11-10 15:23:15 +09:00
SA K
3da477ea0e [FE-chart] Fix an issue where the layout was broken when the table and numeric board boxes were too small or too large #270 2022-11-10 14:59:15 +09:00
SA K
93f13ad478 [FE-chart] Fix issue of missing title and legend option in scatter series charts #269 2022-11-10 13:25:16 +09:00
SA K
1b6fec9f52 [FE-chart] Fix a bug of not receiving single color when modifying in the funnel chart #268 2022-11-10 13:04:52 +09:00
SA K
6f2ebc6b70 [FE] Fix error messages that appear twice 2022-11-10 13:00:51 +09:00
손승우
1eb8ea593c feat: add QTT-002 2022-11-09 20:58:52 +09:00
SA K
e5f029356c Merge pull request #267 from sakang07/develop
Fix bugs
2022-11-09 19:20:38 +09:00
SA K
3d6a5c0f39 Merge branch 'develop' of https://github.com/vanillabrain/vanillameta into develop 2022-11-09 19:19:22 +09:00
SA K
86a0a28dfe [FE-chart] Fix option nesting bugs 2022-11-09 19:18:46 +09:00
SA K
a6533b4ed5 [FE-chart] Fix pie chart series color errors 2022-11-09 19:16:02 +09:00
SA K
4f0166434a [FE-data] Initialize when an error occurs 2022-11-09 19:05:40 +09:00
HeeseonYoon
4bf16cccb9 Merge pull request #266 from yathoo88/develop_backend_yhs
Develop backend yhs
2022-11-09 19:03:40 +09:00
HeeseonYoon
aa139a693e [FE-aggregation] Fix aggregate function logic #265 2022-11-09 19:02:44 +09:00
HeeseonYoon
5be30144b4 [FE-test] Modify aggregate function test file (QTT-004) 2022-11-09 18:54:15 +09:00
HeeseonYoon
889a5c4011 Merge pull request #264 from yathoo88/develop_backend_yhs
[FE-data] Modify select database info service #261
2022-11-09 18:34:39 +09:00
HeeseonYoon
ab31fcb8e7 [FE-data] Modify select database info service #261 2022-11-09 18:33:23 +09:00
HeeseonYoon
37d68ac5d9 Merge pull request #263 from yathoo88/develop_backend_yhs
Develop backend yhs
2022-11-09 18:25:14 +09:00
HeeseonYoon
31e4457f39 Merge remote-tracking branch 'upstream/develop' into develop_backend_yhs 2022-11-09 18:24:26 +09:00
HeeseonYoon
7484ba098b [FE-aggregation] Add aggregate function test file (QTT-004) #262 2022-11-09 18:24:13 +09:00
HeeseonYoon
93417fd355 [BE-database] Add select database info service #261 2022-11-09 18:22:21 +09:00
SA K
6663567c90 Merge pull request #260 from sakang07/develop
Fix Issue
2022-11-09 17:08:49 +09:00
SA K
d327f4fd29 Merge branch 'develop' of https://github.com/vanillabrain/vanillameta into develop 2022-11-09 17:05:37 +09:00
SA K
0106f1a312 [FE-data] Modify query error message 2022-11-09 17:04:24 +09:00
SA K
b8bbced7b7 [FE-data] Add error handling when querying database #259 2022-11-09 16:51:20 +09:00
HeeseonYoon
b163530d67 Merge pull request #258 from yathoo88/develop_backend_yhs
Develop backend yhs
2022-11-09 14:50:26 +09:00
HeeseonYoon
2c8ff8ace9 Merge remote-tracking branch 'upstream/develop' into develop_backend_yhs 2022-11-09 14:49:32 +09:00
HeeseonYoon
4244f30dbd [BE-test] Add template test case ( QTT-06 ) #257 2022-11-09 14:49:17 +09:00
godyuo
71df441572 Merge pull request #256 from godyuo/develop
feat: add QTT-001 testscript
2022-11-09 14:09:59 +09:00
손승우
1267c96de9 feat: add QTT-001 testscript 2022-11-09 14:09:15 +09:00
SA K
465f58092c [FE] Add loading screen where does not appear in some required places #253 2022-11-09 13:40:02 +09:00
HeeseonYoon
7fa7f6b7ea Merge pull request #254 from yathoo88/develop_backend_yhs
[BE-test] Add sample test file ( QTT-05, QTT-06 )
2022-11-09 13:38:20 +09:00
HeeseonYoon
11c45b7766 [BE-test] Add sample test file ( QTT-05, QTT-06 ) 2022-11-09 13:30:38 +09:00
Dosun Yun
c29d639c0d [BE-front] Update API URL 2022-11-09 07:51:52 +09:00
Dosun Yun
ea8f1bc5f6 [BE-setup] Remove IBM Db2 2022-11-09 07:09:06 +09:00
Dosun Yun
0ed2b8176f [BE-setup] Update dockerfile 2022-11-09 03:58:18 +09:00
SA K
8fa19cac4c Merge pull request #252 from sakang07/develop
Add db connection of snowflake and fix bugs
2022-11-08 21:29:18 +09:00
SA K
67a8840c61 [FE-chart] Fix issue with color initialization when modifying color in pie series chart #251 2022-11-08 21:27:09 +09:00
SA K
b511031eea Merge branch 'develop' of https://github.com/vanillabrain/vanillameta into develop 2022-11-08 21:01:06 +09:00
SA K
b70b1e835b [FE-data] Add initial value setting when no data source is selected #250 2022-11-08 21:00:55 +09:00
SA K
ca239605fb [FE-data] Add connection of snowflake #207 2022-11-08 20:26:44 +09:00
HeeseonYoon
762a99cc27 Merge pull request #249 from yathoo88/develop_backend_yhs
Develop backend yhs
2022-11-08 18:48:02 +09:00
HeeseonYoon
c719791d72 Merge remote-tracking branch 'upstream/develop' into develop_backend_yhs 2022-11-08 18:47:06 +09:00
HeeseonYoon
b50e1b644b [BE-common] Jest Set up 2022-11-08 18:46:49 +09:00
SA K
190a4a9a62 Merge pull request #248 from sakang07/develop
Fix bugs and add a feature of snackbar alert
2022-11-08 18:09:06 +09:00
SA K
a6518658cf [FE] Replace some pop-up alert messages with snack bars 2022-11-08 18:03:51 +09:00
SA K
19f14ef4d8 Merge branch 'develop' of https://github.com/vanillabrain/vanillameta into develop 2022-11-08 16:02:44 +09:00
SA K
372997ec0f [FE-data] Add error handling when executing query on dataset with snackbar alert #244 2022-11-08 16:02:31 +09:00
HeeseonYoon
22090ad3da Merge pull request #247 from yathoo88/develop_backend_yhs
Develop backend yhs
2022-11-08 14:49:22 +09:00
HeeseonYoon
b4088b64ae [BE-database] Fixed connect snowflake 2022-11-08 14:48:14 +09:00
HeeseonYoon
1d0ecdeec8 Merge remote-tracking branch 'upstream/develop' into develop_backend_yhs
# Conflicts:
#	backend-api/src/database/database.service.ts
2022-11-08 14:22:40 +09:00
HeeseonYoon
795534a8dc [BE-database] Fixed delete logic - remove relative widgets, table queries and datasets 2022-11-08 14:21:24 +09:00
godyuo
a7843b1b8b Merge pull request #246 from godyuo/develop
feat: add snowflake connection
2022-11-08 13:47:37 +09:00
손승우
8afb791fef feat: add snowflake connection 2022-11-08 13:46:54 +09:00
SA K
89b4ea5ae7 [FE-data] Fix a bug that failed to get database id when creating dataset #245 2022-11-08 11:51:10 +09:00
godyuo
38e0f2a0d6 Merge pull request #243 from godyuo/develop
fix: database Invalid table name
2022-11-08 09:15:04 +09:00
손승우
91ae5ea3b0 fix: database Invalid table name 2022-11-08 09:14:07 +09:00
Jeon Jae Yeoul
29c9ed688e Merge pull request #242 from jyjeun/develop
대시보드 수정 후 대시보드 조회 화면으로 이동
2022-11-07 23:42:56 +09:00
jyjeun
203b4a6ea1 대시보드 수정 후 대시보드 조회 화면으로 이동 2022-11-07 23:41:53 +09:00
Dosun Yun
64415afb8e [BE-database] Add snowflake knex dialect 2022-11-07 23:27:56 +09:00
SA K
e3d37f792b Merge pull request #241 from sakang07/develop
Fix bugs
2022-11-07 19:08:20 +09:00
SA K
17d055b77b Merge branch 'develop' of https://github.com/vanillabrain/vanillameta into dev 2022-11-07 19:05:57 +09:00
SA K
ce165d3ae7 [FE-data] Fix a bug that tooltip is not visible when mixed donut chart and score board mouse over #240 2022-11-07 19:05:16 +09:00
SA K
7dc719e3e6 [FE-data] Add error handling when database and dataset creation or modification fails #239 2022-11-07 18:47:04 +09:00
SA K
69a5b9ff65 Merge pull request #238 from sakang07/develop
Add bigquery and fix bugs
2022-11-07 17:33:51 +09:00
SA K
24d70e3892 Merge branch 'develop' of https://github.com/vanillabrain/vanillameta into develop 2022-11-07 17:28:56 +09:00
SA K
0b0dce3334 [FE-data] Modify engine values to be taken from db 2022-11-07 17:28:36 +09:00
godyuo
d11f74058e Merge pull request #237 from godyuo/develop
style: update cockroach connections database.service
2022-11-07 17:10:19 +09:00
손승우
1752c2b6fa style: update cockroach connections database.service 2022-11-07 17:05:43 +09:00
SA K
41d4386ca7 Merge branch 'develop' of https://github.com/vanillabrain/vanillameta into develop 2022-11-07 17:05:07 +09:00
SA K
3cee7fff91 [FE-data] Fix a bug with engine incorrectly entering 2022-11-07 16:58:00 +09:00
SA K
be67b54371 [FE-data] Fix the bigQuery connection bug #207 2022-11-07 16:51:33 +09:00
SA K
c50f224cfa [FE-data] Add connection of bigQuery #207 2022-11-07 16:42:52 +09:00
godyuo
2480fa8888 Merge pull request #236 from godyuo/develop
fix cockroachdb config
2022-11-07 16:41:02 +09:00
손승우
b9e5b76ec9 fix connection.service cockroachdb config 2022-11-07 16:39:39 +09:00
HeeseonYoon
f4b988d24c Merge pull request #235 from yathoo88/develop_backend_yhs
Develop backend yhs
2022-11-07 15:26:34 +09:00
HeeseonYoon
0d9b0904b6 Merge remote-tracking branch 'upstream/develop' into develop_backend_yhs 2022-11-07 15:23:45 +09:00
HeeseonYoon
63b91a1064 [BE-common] gitignore update (add bigquery-key json file) 2022-11-07 15:23:31 +09:00
SA K
2a59e8b4c4 Merge pull request #234 from sakang07/develop
Fix a bug with incorrect entry of table parameters when creating widget charts
2022-11-07 15:11:14 +09:00
SA K
68aaa7bfeb [FE-widget] Fix a bug with incorrect entry of table parameters when creating widget charts 2022-11-07 15:08:55 +09:00
SA K
072849b577 [FE-data] Add icons of cockroachDB, postGIS 2022-11-07 14:24:37 +09:00
SA K
b7a847e2a7 Merge branch 'develop' of https://github.com/vanillabrain/vanillameta into develop 2022-11-07 13:27:35 +09:00
SA K
17fcd7f44c [FE] Add favicon, modify the widget card style, fix a bug in the gauge chart 2022-11-07 13:27:26 +09:00
BongheeCha
8a7a4f4286 Update README.md 2022-11-07 02:02:59 +09:00
BongheeCha
1b51436770 Upload Logo File 2022-11-07 02:01:33 +09:00
BongheeCha
cabfaae464 Upload Readme Images 2022-11-07 01:50:59 +09:00
Gaedong Kim
c4ee702da3 Update README.md
modified DB2 to Db2
2022-11-05 12:24:56 +09:00
SA K
64159665ab [FE-widget] Modify the table option to receive the string 2022-11-04 18:02:31 +09:00
SA K
719c9723e3 [FE-widget] Modify chart layout 2022-11-04 17:56:48 +09:00
SA K
3ee14ed7ca [FE-widget] Fix a bug that options were duplicated when fixing the scoreboard #232 2022-11-04 17:43:08 +09:00
SA K
f327390765 Merge pull request #231 from sakang07/develop
Fix bugs about widget
2022-11-04 15:36:59 +09:00
SA K
3aa422c127 [FE-widget] Fix a bug that do not return to the widget view page when editing on the widget view page #230 2022-11-04 15:25:54 +09:00
SA K
92a5786974 [FE-widget/theme] Add sort to chart creation, add link in footer copyright, modify theme 2022-11-04 15:25:45 +09:00
SA K
8ab3f49690 [FE-widget] Fix problems that do not receive new lists after adding widgets, add a loading spinner in dashboard page #229 2022-11-04 15:25:25 +09:00
Sangwon Lee
2aabd45efa Merge pull request #228 from e8133/develop
Add jest lib to test Front-end
2022-11-04 13:04:55 +09:00
lsw
d62d9baedf Merge branch 'develop' of https://github.com/vanillabrain/vanillameta into develop 2022-11-04 13:02:56 +09:00
lsw
796a576b8a Add jest for Front-end test 2022-11-04 13:02:50 +09:00
HeeseonYoon
d332dfb9db Merge pull request #226 from yathoo88/develop_backend_yhs
Develop backend yhs
2022-11-03 19:04:31 +09:00
HeeseonYoon
6feae672fe Merge remote-tracking branch 'upstream/develop' into develop_backend_yhs 2022-11-03 19:02:39 +09:00
HeeseonYoon
033979fd39 [BE-template] Modify template layout setting #225 - Change the priority of widget items 'widgetList' that are exceeded in the template 2022-11-03 19:02:14 +09:00
HeeseonYoon
6051f8b1e6 Merge pull request #224 from yathoo88/develop_backend_yhs
Develop backend yhs
2022-11-03 18:22:32 +09:00
HeeseonYoon
7df10f97cf Merge remote-tracking branch 'upstream/develop' into develop_backend_yhs 2022-11-03 18:19:31 +09:00
HeeseonYoon
466c3bacd7 [BE-database] Add Connect db: connect oracle #167 2022-11-03 18:19:19 +09:00
godyuo
df4016f9f9 Merge pull request #223 from godyuo/develop
update redshift table
2022-11-03 16:33:02 +09:00
손승우
d5cd5b7b40 update redshift table 2022-11-03 16:31:36 +09:00
SA K
f9c3307426 Merge pull request #222 from sakang07/develop
Develop
2022-11-03 13:36:28 +09:00
SA K
a621288e5c [FE-layout] Fix layout 2022-11-03 13:35:05 +09:00
SA K
48ba06580f [FE-login] Change login background image 2022-11-03 12:57:29 +09:00
SA K
0a47bcb096 [FE-login] Add features to log in continuously during a session #221 2022-11-03 12:56:51 +09:00
HeeseonYoon
60fed38eb5 Merge pull request #220 from yathoo88/develop_backend_yhs
Develop backend yhs
2022-11-03 11:47:27 +09:00
HeeseonYoon
b62a75b2e6 Merge remote-tracking branch 'upstream/develop' into develop_backend_yhs 2022-11-03 11:46:37 +09:00
HeeseonYoon
bb535ced21 [BE-database] Add Connect db: connect Bigquery #193 2022-11-03 11:46:21 +09:00
Dosun Yun
ee7c75ed0c oracle driver 설치 스크립트 2022-11-03 00:27:34 +09:00
Dosun Yun
e3e231c011 oracle-db test query 추가 2022-11-03 00:00:38 +09:00
Dosun Yun
fff462be1b oracle-db test query 추가 2022-11-02 23:50:10 +09:00
SA K
d49df5b747 Merge pull request #219 from sakang07/develop
Fix bugs and apply design
2022-11-02 19:16:39 +09:00
SA K
c1c181229d Merge branch 'develop' of https://github.com/vanillabrain/vanillameta into develop 2022-11-02 19:13:21 +09:00
SA K
f0d73555b4 [FE-login] Modify design of login page #218 2022-11-02 19:13:11 +09:00
HeeseonYoon
c21bf84f37 Merge pull request #217 from yathoo88/develop_backend_yhs
Develop backend yhs
2022-11-02 19:08:28 +09:00
HeeseonYoon
13e306b9d4 Merge remote-tracking branch 'upstream/develop' into develop_backend_yhs 2022-11-02 19:07:40 +09:00
HeeseonYoon
fe5f8922e9 [BE-database] Add Connect db: connect Bigquery #193 2022-11-02 19:07:27 +09:00
SA K
65b217518f [FE-data] Fix problem of not getting id of db when modifying dataset #208 2022-11-02 17:48:23 +09:00
SA K
09bbb1d53d [FE-data] Fix missing parameters when saving database 2022-11-02 17:37:44 +09:00
SA K
614863bf9d [FE-data] Edit database api parameter and fix bugs 2022-11-02 17:20:38 +09:00
HeeseonYoon
eba1fab8cc Merge pull request #216 from yathoo88/develop_backend_yhs
[BE-database] Add databaseType column #215
2022-11-02 14:34:34 +09:00
HeeseonYoon
dc53fe51db [BE-database] Add databaseType column #215 2022-11-02 14:33:39 +09:00
Jeon Jae Yeoul
2649a88731 Merge pull request #214 from jyjeun/develop
Develop
2022-11-02 13:44:27 +09:00
jjy
4c79e17ce0 Merge branch 'develop' of https://github.com/vanillabrain/vanillameta into develop_jyjeun 2022-11-02 13:43:30 +09:00
jjy
3ae9542666 TextField
- label : 대시보드 이름
2022-11-02 13:43:18 +09:00
jjy
9bdfaace16 위젯 배치 로직 수정
- 중복로직 제거
2022-11-02 13:41:21 +09:00
godyuo
a8244eb40f Merge pull request #213 from godyuo/develop
add connect Redshift #192
2022-11-02 11:45:58 +09:00
손승우
5a186ebe14 add connect Redshift #192 2022-11-02 11:43:43 +09:00
HeeseonYoon
560329410e Merge pull request #212 from yathoo88/develop_backend_yhs
Develop backend yhs
2022-11-02 11:10:05 +09:00
HeeseonYoon
4473e1250e Merge remote-tracking branch 'upstream/develop' into develop_backend_yhs 2022-11-02 11:08:54 +09:00
HeeseonYoon
dac910fbf0 [BE-database] Fixed bug #211 - Can not read table list 2022-11-02 11:08:41 +09:00
Jeon Jae Yeoul
cb38228906 Merge pull request #210 from jyjeun/develop
대시보드에 위젯 추가시 비어있는 공간으로 추가되게 수정
2022-11-02 01:38:47 +09:00
jyjeun
01479160d5 대시보드 위젯 추가시 빈공간에 추가되는 위젯 들어가게 변경.
- 신규 위젯 기본크기 : w -> 6, h -> 5
 - 신규 위젯 기본크기 변경하고 싶을 시 getCalculatorPosition 함수에서 기본크기 변경 가능.
 - layout 최대크기(width) 변경하고 싶을 시 (현재 최대크기 12) getCalculatorPosition 함수에서 최대 width 값 변경 가능
2022-11-02 01:37:34 +09:00
jyjeun
0433dcccdb Merge branch 'develop' of https://github.com/vanillabrain/vanillameta into develop_jyjeun 2022-11-01 22:52:49 +09:00
SA K
1a2bb1b22c Merge pull request #209 from sakang07/develop
Add a feature in data page
2022-11-01 22:07:38 +09:00
SA K
ad1f7c659c Merge branch 'develop' of https://github.com/vanillabrain/vanillameta into develop 2022-11-01 21:55:58 +09:00
SA K
41c55d97c2 [FE-data] Invoke form differently depending on db source and whether it is created or modified: mysql, mariaDb, pg, mssql, sqlite #207 2022-11-01 21:55:45 +09:00
HeeseonYoon
7fbf4d3530 Merge pull request #206 from yathoo88/develop_backend_yhs
[BE-database] Fixed data load Error #205 - can not load table info when datasetType is TABLE
2022-11-01 17:48:50 +09:00
HeeseonYoon
2c7ae3c9f0 Merge remote-tracking branch 'upstream/develop' into develop_backend_yhs 2022-11-01 17:46:52 +09:00
HeeseonYoon
b68365c99b [BE-database] Fixed data load Error #205 - can not load table info when datasetType is TABLE 2022-11-01 17:46:34 +09:00
Sangwon Lee
c8730bc587 Merge pull request #204 from e8133/develop
Add login
2022-11-01 15:11:06 +09:00
lsw
1fa0d95cb0 Add Login 2022-11-01 15:10:14 +09:00
lsw
2da217f3d0 Merge branch 'develop' of https://github.com/vanillabrain/vanillameta into develop 2022-11-01 10:33:38 +09:00
jyjeun
4b70915182 Merge branch 'develop' of https://github.com/vanillabrain/vanillameta into develop_jyjeun 2022-10-30 17:39:29 +09:00
jyjeun
a3f25ae2ad 레이아웃 작업중 2022-10-30 17:37:22 +09:00
lsw
29807adc97 Merge branch 'develop' of https://github.com/vanillabrain/vanillameta into develop 2022-10-28 13:54:32 +09:00
lsw
17c9bc5463 Merge branch 'develop' of https://github.com/vanillabrain/vanillameta into develop 2022-10-28 13:18:12 +09:00
lsw
43ed27163e Merge branch 'develop' of https://github.com/vanillabrain/vanillameta into develop
 Conflicts:
	backend-api/package-lock.json
2022-10-27 17:03:43 +09:00
lsw
3eca3291c7 Modify widget button 2022-10-27 17:03:21 +09:00
140 changed files with 99962 additions and 1423 deletions

View File

@@ -1,5 +1,7 @@
# VanillaMeta
<img title="VanillaMeta Logo" src="design/vanillameta-logo.png"/><br/>
최신 엔터프라이즈용 비즈니스 인텔리전스 웹 애플리케이션입니다.
# 바닐라메타를 사용해야 하는 이유
@@ -17,10 +19,17 @@
## 스크린샷
- 다양한 시각화 차트
- 강력한 SQL 편집기
- 코딩없이 차트 제작
- 템플릿 추천
- **다양한 시각화 차트**
<kbd><img title="Chart" src="design/feature-01.png"/></kbd><br/>
- **강력한 SQL 편집기**
<kbd><img title="Chart" src="design/feature-02.png"/></kbd><br/>
- **코딩없이 차트 제작**
<kbd><img title="Chart" src="design/feature-03.png"/></kbd><br/>
- **템플릿 추천**
<kbd><img title="Chart" src="design/feature-04.png"/></kbd><br/>
## 지원하는 데이터베이스
@@ -30,6 +39,7 @@
- SQLServer
- SQLite
- Oracle
- DB2
- Amazon Redshift
- Big Query
- Cockroachdb
- Snowflake

119
backend-api/.dockerignore Normal file
View File

@@ -0,0 +1,119 @@
# Created by .ignore support plugin (hsz.mobi)
### Node template
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variables file
.env
.env.test
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*

View File

@@ -1,8 +1,8 @@
module.exports = {
parser: '@typescript-eslint/parser',
parserOptions: {
project: 'tsconfig.json',
tsconfigRootDir : __dirname,
project: './tsconfig.json',
tsconfigRootDir : __dirname,
sourceType: 'module',
},
plugins: ['@typescript-eslint/eslint-plugin'],
@@ -22,4 +22,4 @@ module.exports = {
'@typescript-eslint/explicit-module-boundary-types': 'off',
'@typescript-eslint/no-explicit-any': 'off',
},
};
};

View File

@@ -4,6 +4,8 @@ config.serverless.yml
.env.prod
.ormconfig.json
vanillameta
bigquery-key.json
test-connect-info.json
.
# compiled output
/dist

View File

@@ -1,24 +1,19 @@
FROM ubuntu:18.04
RUN apt-get -qq update
RUN apt-get -qq upgrade --yes
RUN apt-get -qq install curl --yes
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash -
RUN apt-get -qq install nodejs --yes
RUN apt-get install chromium-browser --yes
FROM node:14-slim
# ORACLE 설치
RUN apt-get update && apt-get install -y libaio1 wget unzip
WORKDIR /opt/oracle
RUN wget https://download.oracle.com/otn_software/linux/instantclient/instantclient-basiclite-linuxx64.zip && \
unzip instantclient-basiclite-linuxx64.zip && rm -f instantclient-basiclite-linuxx64.zip && \
cd /opt/oracle/instantclient* && rm -f *jdbc* *occi* *mysql* *mql1* *ipc1* *jar uidrvci genezi adrci && \
echo /opt/oracle/instantclient* > /etc/ld.so.conf.d/oracle-instantclient.conf && ldconfig
WORKDIR /app
COPY package*.json ./
RUN npm install
# ENV NODE_ENV production
# COPY --from=builder /app ./
COPY . .
COPY tsconfig.json .
COPY tsconfig.build.json .
RUN npm install
RUN npm run build
EXPOSE 3000
# expose our default runtime port
EXPOSE 4000
CMD ["npm", "run", "start"]
CMD ["npm","run","start:prod"]

View File

@@ -0,0 +1,30 @@
FROM ubuntu:18.04
RUN apt-get -qq update
RUN apt-get -qq upgrade --yes
RUN apt-get -qq install curl --yes
RUN curl -sL https://deb.nodesource.com/setup_14.x | bash -
RUN apt-get -qq install nodejs --yes
RUN apt-get install chromium-browser --yes
RUN useradd -ms /bin/bash frog
USER frog
WORKDIR /home/frog
COPY package*.json ./
RUN npm install
COPY . .
COPY tsconfig.json .
COPY tsconfig.build.json .
RUN npm run build
EXPOSE 3000
CMD ["npm", "run", "start"]

View File

@@ -3,44 +3,46 @@ services:
main:
build:
context: ./
dockerfile: ./Dockerfile
dockerfile: ./Dockerfile.dev
ports:
- "4000:4000"
networks:
- vanillameta
depends_on:
- mysql
# - mysql
# - pg
- mssql
links:
- "mysql:mysqldb"
# - "mysql:mysqldb"
# - "pg"
- mssql
env_file:
- .env
- .env.dev
restart: always
mysql:
container_name: vanillameta_mysql
image: mysql
ports:
- "3306:3306"
networks:
- vanillameta
environment:
MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
MYSQL_CHARSET: utf8mb4
MYSQL_DATABASE: ${DB_NAME}
MYSQL_USER: ${DB_USERNAME}
MYSQL_PASSWORD: ${DB_PASSWORD}
TZ: Asia/Seoul
restart: always
command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
--default_authentication_plugin=mysql_native_password
volumes:
- mysql:/var/lib/mysql
- ./db/conf.d:/etc/mysql/conf.d
# mysql:
# container_name: vanillameta_mysql
# image: mysql
# ports:
# - "3306:3306"
# networks:
# - vanillameta
#
# environment:
# MYSQL_ROOT_PASSWORD: ${DB_ROOT_PASSWORD}
# MYSQL_CHARSET: utf8mb4
# MYSQL_DATABASE: ${DB_NAME}
# MYSQL_USER: ${DB_USERNAME}
# MYSQL_PASSWORD: ${DB_PASSWORD}
# TZ: Asia/Seoul
# restart: always
# command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
# --default_authentication_plugin=mysql_native_password
#
# volumes:
# - mysql:/var/lib/mysql
# - ./db/conf.d:/etc/mysql/conf.d
# mariadb:
# container_name: vanillameta_mariadb
@@ -104,25 +106,27 @@ services:
# - ./db/conf.d:/etc/porstgressql/data
#
# mssql:
# container_name: vanillameta_mssql
# image: mcr.microsoft.com/azure-sql-edge
# ports:
# - "1433:1433"
# networks:
# - vanillameta
# environment:
# ACCEPT_EULA: "Y"
# MSSQL_SA_PASSWORD: ${DB_PASSWORD}
# MSSQL_AGENT_ENABLED: "true"
# restart: always
# command: --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
#
# volumes:
# - ./vanillameta_mssqldata:/var/opt/mssql
#
# - ./db/conf.d:/etc/mysql/conf.d
mssql:
container_name: vanillameta_mssql
image: mcr.microsoft.com/mssql/server:2019-latest
user: frog
ports:
- "1433:1433"
networks:
- vanillameta
environment:
ACCEPT_EULA: "Y"
MSSQL_SA_PASSWORD: 'Qkslffk@123123'
restart: always
volumes:
- ./sqlvolume:/home/frog
volumes:
mysql:

View File

@@ -0,0 +1,10 @@
#!/bin/sh
BASEDIR=$(PWD)
cd $HOME/Downloads
curl -O https://download.oracle.com/otn_software/mac/instantclient/198000/instantclient-basic-macos.x64-19.8.0.0.0dbru.dmg
hdiutil mount instantclient-basic-macos.x64-19.8.0.0.0dbru.dmg
/Volumes/instantclient-basic-macos.x64-19.8.0.0.0dbru/install_ic.sh
hdiutil unmount /Volumes/instantclient-basic-macos.x64-19.8.0.0.0dbru
cd $BASEDIR
cp ~/Downloads/instantclient_19_8/{libclntsh.dylib.19.1,libclntshcore.dylib.19.1,libnnz19.dylib,libociei.dylib} node_modules/oracledb/build/Release
cd node_modules/oracledb/build/Release/ && ln -s libclntsh.dylib.19.1 libclntsh.dylib

View File

@@ -1,9 +0,0 @@
{
"moduleFileExtensions": ["js", "json", "ts"],
"rootDir": ".",
"testEnvironment": "node",
"testRegex": ".*spec.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"
}
}

View File

@@ -1,29 +0,0 @@
[
{
"name": "default",
"type": "sqlite",
"host" : "127.0.0.1",
"post" : 5432,
"username" : "username",
"password" : "password",
"database" : "db_01",
"entities": [
"dist/**/*.entity.js"
]
},
{
"name": "test_db",
"tpye":
"database" : "vanillameta",
},
{
"name": "db_02",
"type": "mysql",
"host": "localhost",
"port": 3306,
"username": "vanillameta",
"password": "pw",
"database": "vanillameta",
"autoLoadEntities": true
]

View File

@@ -4951,14 +4951,6 @@
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
"dev": true
},
"asn1": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
"integrity": "sha512-ix/FxPn0MDjeyJ7i/yoHGFt/EX6LyNbxSEhPPXODPL+KB0VPk86UYfL0lMdy+KCnv+fmvIzySwaK5COwqVbWTQ==",
"requires": {
"safer-buffer": "~2.1.0"
}
},
"asn1.js": {
"version": "5.4.1",
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-5.4.1.tgz",
@@ -5020,11 +5012,6 @@
}
}
},
"assert-plus": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz",
"integrity": "sha512-NfJ4UzBCcQGLDlQq7nHxH+tv3kyZ0hHQqF5BO6J7tNJeP5do1llPr8dZ8zHonfhAu0PHAdMkSo+8o0wxg9lZWw=="
},
"assign-symbols": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/assign-symbols/-/assign-symbols-1.0.0.tgz",
@@ -5153,16 +5140,6 @@
"type-is": "^1.6.16"
}
},
"aws-sign2": {
"version": "0.7.0",
"resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.7.0.tgz",
"integrity": "sha512-08kcGqnYf/YmjoRhfxyu+CLxBjUtHLXLXX/vUfx9l2LYzG3c1m61nrpyFUZI6zeS+Li/wWMMidD9KgrqtGq3mA=="
},
"aws4": {
"version": "1.11.0",
"resolved": "https://registry.npmjs.org/aws4/-/aws4-1.11.0.tgz",
"integrity": "sha512-xh1Rl34h6Fi1DC2WWKfxUTVqRsNnr6LsKz2+hfwDxQJWmrx8+c7ylaqBMcHfl1U1r2dsifOvKX3LQuLNZ+XSvA=="
},
"axios": {
"version": "0.27.2",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.27.2.tgz",
@@ -5606,14 +5583,6 @@
"resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz",
"integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="
},
"bcrypt-pbkdf": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.2.tgz",
"integrity": "sha512-qeFIXtP4MSoi6NLqO12WfqARWWuCKi2Rn/9hJLEmtB5yTNr9DqFWkJRCf2qShWzPeAMRnOgCrq0sg/KLv5ES9w==",
"requires": {
"tweetnacl": "^0.14.3"
}
},
"better-sqlite3": {
"version": "7.6.2",
"resolved": "https://registry.npmjs.org/better-sqlite3/-/better-sqlite3-7.6.2.tgz",
@@ -6341,11 +6310,6 @@
"integrity": "sha512-DdUCktgMSM+1ndk9EFMZcavsGszV7zxV9O7MtOHniTa/iyAIwJCF0dFVBdU9SijJbfh29hC9bCs07wu8pjnGJQ==",
"dev": true
},
"caseless": {
"version": "0.12.0",
"resolved": "https://registry.npmjs.org/caseless/-/caseless-0.12.0.tgz",
"integrity": "sha512-4tYFyifaFfGacoiObjJegolkwSU4xQNGbVgUiNYVUxbQ2x2lUsFvY4hVgVzGiIe6WLOPqycWXA40l+PWsxthUw=="
},
"chainsaw": {
"version": "0.1.0",
"resolved": "https://registry.npmjs.org/chainsaw/-/chainsaw-0.1.0.tgz",
@@ -6842,6 +6806,11 @@
"integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==",
"dev": true
},
"complex.js": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.1.1.tgz",
"integrity": "sha512-8njCHOTtFFLtegk6zQo0kkVX1rngygb/KQI6z1qZxlFI3scluC+LVTCFbrkWjBv4vvLlbQ9t88IPMC6k95VTTg=="
},
"component-bind": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/component-bind/-/component-bind-1.0.0.tgz",
@@ -7195,14 +7164,6 @@
"integrity": "sha512-Vy4dx7gquTeMcQR/hDkYLGUnwVil6vk4FOOct+djUnHOUWt+zJPJAaRIXaAFkPXtJjvlY7o3rfRu0/3hpnwoUA==",
"dev": true
},
"dashdash": {
"version": "1.14.1",
"resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.1.tgz",
"integrity": "sha512-jRFi8UDGo6j+odZiEpjazZaWqEal3w/basFjQHQEwVtZJGDpxbH1MeYluwCS8Xq5wmLJooDlMgvVarmWfGM44g==",
"requires": {
"assert-plus": "^1.0.0"
}
},
"data-uri-to-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-3.0.1.tgz",
@@ -7227,6 +7188,11 @@
"ms": "2.0.0"
}
},
"decimal.js": {
"version": "10.4.2",
"resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.4.2.tgz",
"integrity": "sha512-ic1yEvwT6GuvaYwBLLY6/aFFgjZdySKTE8en/fkU3QICTmRtgtSlFn0u0BXN06InZwtfCelR7j8LRiDI/02iGA=="
},
"decode-uri-component": {
"version": "0.2.0",
"resolved": "https://registry.npmjs.org/decode-uri-component/-/decode-uri-component-0.2.0.tgz",
@@ -7810,15 +7776,6 @@
"resolved": "https://registry.npmjs.org/dylib-node/-/dylib-node-1.0.10.tgz",
"integrity": "sha512-bjsSQJgDz8Iqd0avdq1UmIgL46Ip+WfqJ6Y4AlNGBnZVlMzktQOrwuWYesqRaOTdtGjm8PijjATa8EZBzvgk4g=="
},
"ecc-jsbn": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.2.tgz",
"integrity": "sha512-eh9O+hwRHNbG4BLTjEl3nw044CkGm5X6LoaCf7LPp7UU8Qrt47JYNi6nPX8xjW97TKGKm1ouctg0QSpZe9qrnw==",
"requires": {
"jsbn": "~0.1.0",
"safer-buffer": "^2.1.0"
}
},
"ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
@@ -8083,11 +8040,6 @@
"es6-symbol": "^3.1.1"
}
},
"es6-promise": {
"version": "4.2.8",
"resolved": "https://registry.npmjs.org/es6-promise/-/es6-promise-4.2.8.tgz",
"integrity": "sha512-HJDGx5daxeIvxdBxvG2cb9g4tEvwIk3i8+nhX0yGrYmZUzbkdg8QbDevheDB8gd0//uPj4c1EQua8Q+MViT0/w=="
},
"es6-promisify": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-6.1.1.tgz",
@@ -8140,6 +8092,11 @@
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
},
"escape-latex": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz",
"integrity": "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw=="
},
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
@@ -8708,15 +8665,11 @@
}
}
},
"extsprintf": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz",
"integrity": "sha512-11Ndz7Nv+mvAC1j0ktTa7fAb0vLyGGX+rMHNBYQviQDGU0Hw7lhctJANqbPhu9nV9/izT/IntTgZ7Im/9LJs9g=="
},
"fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"dev": true
},
"fast-diff": {
"version": "1.2.0",
@@ -8740,7 +8693,8 @@
"fast-json-stable-stringify": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz",
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw=="
"integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==",
"dev": true
},
"fast-levenshtein": {
"version": "2.0.6",
@@ -9092,11 +9046,6 @@
"for-in": "^1.0.1"
}
},
"forever-agent": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz",
"integrity": "sha512-j0KLYPhm6zeac4lz3oJ3o65qvgQCcPubiyotZrXqEaG4hNagNYO8qdlUrX5vwqv9ohqeT/Z3j6+yW067yWWdUw=="
},
"fork-ts-checker-webpack-plugin": {
"version": "7.2.13",
"resolved": "https://registry.npmjs.org/fork-ts-checker-webpack-plugin/-/fork-ts-checker-webpack-plugin-7.2.13.tgz",
@@ -9169,6 +9118,11 @@
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
"integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow=="
},
"fraction.js": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.2.0.tgz",
"integrity": "sha512-MhLuK+2gUcnZe8ZHlaaINnQLl0xRIGRfcGk2yl8xoQAfHrSsL3rYu6FCmBdkdbhc9EPlwyGHewaRsvwRMJtAlA=="
},
"fragment-cache": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/fragment-cache/-/fragment-cache-0.2.1.tgz",
@@ -9505,14 +9459,6 @@
"resolved": "https://registry.npmjs.org/getopts/-/getopts-2.3.0.tgz",
"integrity": "sha512-5eDf9fuSXwxBL6q5HX+dhDj+dslFGWzU5thZ9kNKUkcPtaPdatmUFKwHFrLb/uf/WpA4BHET+AX3Scl56cAjpA=="
},
"getpass": {
"version": "0.1.7",
"resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.7.tgz",
"integrity": "sha512-0fzj9JxOLfJ+XGLhR8ze3unN0KZCgZwiSSDz168VERjK8Wl8kVSdcu2kspd4s4wtAa1y/qrVRiAA0WclVsu0ng==",
"requires": {
"assert-plus": "^1.0.0"
}
},
"github-from-package": {
"version": "0.0.0",
"resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz",
@@ -9826,38 +9772,6 @@
}
}
},
"har-schema": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/har-schema/-/har-schema-2.0.0.tgz",
"integrity": "sha512-Oqluz6zhGX8cyRaTQlFMPw80bSJVG2x/cFb8ZPhUILGgHka9SsokCCOQgpveePerqidZOrT14ipqfJb7ILcW5Q=="
},
"har-validator": {
"version": "5.1.5",
"resolved": "https://registry.npmjs.org/har-validator/-/har-validator-5.1.5.tgz",
"integrity": "sha512-nmT2T0lljbxdQZfspsno9hgrG3Uir6Ks5afism62poxqBM6sDnMEuPmzTq8XN0OEwqKLLdh1jQI3qyE66Nzb3w==",
"requires": {
"ajv": "^6.12.3",
"har-schema": "^2.0.0"
},
"dependencies": {
"ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"requires": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
}
},
"json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg=="
}
}
},
"has": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/has/-/has-1.0.3.tgz",
@@ -10090,16 +10004,6 @@
}
}
},
"http-signature": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.2.0.tgz",
"integrity": "sha512-CAbnr6Rz4CYQkLYUtSNXxQPUH2gK8f3iWexVlsnMeD+GjlsQ0Xsy1cOX+mN3dtxYomRy21CiOzU8Uhw6OwncEQ==",
"requires": {
"assert-plus": "^1.0.0",
"jsprim": "^1.2.2",
"sshpk": "^1.7.0"
}
},
"http2-wrapper": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-1.0.3.tgz",
@@ -10687,7 +10591,8 @@
"is-typedarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz",
"integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA=="
"integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==",
"dev": true
},
"is-unc-path": {
"version": "1.0.0",
@@ -10745,11 +10650,6 @@
"integrity": "sha512-BhBvN2MBpWTaSHdWRb/bwdZJ1WaehQ2L1KngkCkfLUGF0mAWAT1sQUQacEmQ0jXkFw/czDXPNQSL5u2/Krsz1w==",
"dev": true
},
"isstream": {
"version": "0.1.2",
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
"integrity": "sha512-Yljz7ffyPbrLpLngrMtZ7NduUgVvi6wG9RJ9IUcyCd59YQ911PBJphODUcbOVbqYfxe1wuYf/LJ8PauMRwsM/g=="
},
"istanbul-lib-coverage": {
"version": "3.2.0",
"resolved": "https://registry.npmjs.org/istanbul-lib-coverage/-/istanbul-lib-coverage-3.2.0.tgz",
@@ -10843,6 +10743,11 @@
"integrity": "sha512-gZmQKe1QrfkkMjCn8Qv9cpyJFyogTYqkP5WCobX5RNaHsJzIV/6NvAnlnouOcwKr29QrxLGDGcqYuJ+ae98s1A==",
"dev": true
},
"javascript-natural-sort": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
"integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw=="
},
"jest": {
"version": "28.1.3",
"resolved": "https://registry.npmjs.org/jest/-/jest-28.1.3.tgz",
@@ -11444,11 +11349,6 @@
"resolved": "https://registry.npmjs.org/jsbi/-/jsbi-4.3.0.tgz",
"integrity": "sha512-SnZNcinB4RIcnEyZqFPdGPVgrg2AcnykiBy0sHVJQKHYeaLUvi3Exj+iaPpLnFVkDPZIV4U0yvgC9/R4uEAZ9g=="
},
"jsbn": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.1.tgz",
"integrity": "sha512-UVU9dibq2JcFWxQPA6KCqj5O42VOmAY3zQUfEKxU0KpTGXwNoCjkX1e13eHNvw/xPynt6pU0rZ1htjWTNTSXsg=="
},
"jsesc": {
"version": "2.5.2",
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz",
@@ -11518,11 +11418,6 @@
}
}
},
"json-schema": {
"version": "0.4.0",
"resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.4.0.tgz",
"integrity": "sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA=="
},
"json-schema-traverse": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz",
@@ -11544,11 +11439,6 @@
"integrity": "sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==",
"dev": true
},
"json-stringify-safe": {
"version": "5.0.1",
"resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz",
"integrity": "sha512-ZClg6AaYvamvYEE82d3Iyd3vSSIjQ+odgjaTzRuO3s7toCdFKczob2i0zCh7JE8kWn17yvAWhUVxvqGwUalsRA=="
},
"json5": {
"version": "2.2.1",
"resolved": "https://registry.npmjs.org/json5/-/json5-2.2.1.tgz",
@@ -11624,17 +11514,6 @@
}
}
},
"jsprim": {
"version": "1.4.2",
"resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.4.2.tgz",
"integrity": "sha512-P2bSOMAc/ciLz6DzgjVlGJP9+BrJWu5UDGK70C2iweC5QBIeFf0ZXRvGjEj2uYgrY2MkAAhsSWHDWlFtEroZWw==",
"requires": {
"assert-plus": "1.0.0",
"extsprintf": "1.3.0",
"json-schema": "0.4.0",
"verror": "1.10.0"
}
},
"jszip": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/jszip/-/jszip-3.10.1.tgz",
@@ -12056,125 +11935,6 @@
"lodash.isnil": "^4.0.0"
}
},
"knex-snowflake-dialect": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/knex-snowflake-dialect/-/knex-snowflake-dialect-1.0.1.tgz",
"integrity": "sha512-MKXc2alsv9bt/TkiUdLD40Pp4CQJikbNztOJawvT2+tppXkvSywQL9agKE2jH8UqEZuUg47Gp1U7Thvg+yqx5g==",
"requires": {
"bluebird": "^3.7.2",
"lodash": "^4.17.15",
"snowflake-sdk": "1.6.0"
},
"dependencies": {
"agent-base": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-2.1.1.tgz",
"integrity": "sha512-oDtZV740o3fr5oJtPLOsgH2hl2TRPscNXIx4VzzBwVlXVkv8RHm7XXqGAYg8t20+Gwu6LNDnx8HRMGqVGPZ8Vw==",
"requires": {
"extend": "~3.0.0",
"semver": "~5.0.1"
}
},
"axios": {
"version": "0.21.4",
"resolved": "https://registry.npmjs.org/axios/-/axios-0.21.4.tgz",
"integrity": "sha512-ut5vewkiu8jjGBdqpM44XxjuCjq9LAKeHVmoVfHVzy8eHgxxq8SbAVQNovDA8mVi05kP0Ea/n/UzcSHcTJQfNg==",
"requires": {
"follow-redirects": "^1.14.0"
}
},
"bignumber.js": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-2.4.0.tgz",
"integrity": "sha512-uw4ra6Cv483Op/ebM0GBKKfxZlSmn6NgFRby5L3yGTlunLj53KQgndDlqy2WVFOwgvurocApYkSud0aO+mvrpQ=="
},
"debug": {
"version": "3.2.7",
"resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz",
"integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==",
"requires": {
"ms": "^2.1.1"
}
},
"es6-promisify": {
"version": "5.0.0",
"resolved": "https://registry.npmjs.org/es6-promisify/-/es6-promisify-5.0.0.tgz",
"integrity": "sha512-C+d6UdsYDk0lMebHNR4S2NybQMMngAOnOwYBQjTOiv0MkoJMP0Myw2mgpDLBcpfCmRLxyFqYhS/CfOENq4SJhQ==",
"requires": {
"es6-promise": "^4.0.3"
}
},
"https-proxy-agent": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-3.0.1.tgz",
"integrity": "sha512-+ML2Rbh6DAuee7d07tYGEKOEi2voWPUGan+ExdPbPW6Z3svq+JCqr0v8WmKPOkz1vOVykPCBSuobe7G8GJUtVg==",
"requires": {
"agent-base": "^4.3.0",
"debug": "^3.1.0"
},
"dependencies": {
"agent-base": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/agent-base/-/agent-base-4.3.0.tgz",
"integrity": "sha512-salcGninV0nPrwpGNn4VTXBb1SOuXQBiqbrNXoeizJsHrsL6ERFM2Ne3JUSBWRE6aeNJI2ROP/WEEIDUiDe3cg==",
"requires": {
"es6-promisify": "^5.0.0"
}
}
}
},
"mkdirp": {
"version": "1.0.4",
"resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz",
"integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw=="
},
"ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"semver": {
"version": "5.0.3",
"resolved": "https://registry.npmjs.org/semver/-/semver-5.0.3.tgz",
"integrity": "sha512-5OkOBiw69xqmxOFIXwXsiY1HlE+om8nNptg1ZIf95fzcnfgOv2fLm7pmmGbRJsjJIqPpW5Kwy4wpDBTz5wQlUw=="
},
"snowflake-sdk": {
"version": "1.6.0",
"resolved": "https://registry.npmjs.org/snowflake-sdk/-/snowflake-sdk-1.6.0.tgz",
"integrity": "sha512-5hjK1/swkYH5/+Q2ozcknhdKzTABviA9ncj3Go6rGVhNz2iXZDv+wC1dWlq3XYFKYhjPEd45SyhAZ20+yWgAbA==",
"requires": {
"agent-base": "^2.1.1",
"asn1.js-rfc2560": "^5.0.0",
"asn1.js-rfc5280": "^3.0.0",
"axios": "^0.21.1",
"big-integer": "^1.6.43",
"bignumber.js": "^2.4.0",
"browser-request": "^0.3.3",
"debug": "^3.2.6",
"extend": "^3.0.2",
"https-proxy-agent": "^3.0.0",
"jsonwebtoken": "^8.5.1",
"lodash": "^4.17.21",
"mkdirp": "^1.0.3",
"mock-require": "^3.0.3",
"moment": "^2.23.0",
"moment-timezone": "^0.5.15",
"ocsp": "^1.2.0",
"open": "^7.3.1",
"request": "^2.88.0",
"requestretry": "^4.1.0",
"simple-lru-cache": "^0.0.2",
"uuid": "^3.3.2",
"winston": "^3.1.0"
}
},
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
}
}
},
"kuler": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz",
@@ -12561,6 +12321,37 @@
"object-visit": "^1.0.0"
}
},
"mathjs": {
"version": "11.3.3",
"resolved": "https://registry.npmjs.org/mathjs/-/mathjs-11.3.3.tgz",
"integrity": "sha512-+NsgPRzvnczrw5hp7fNPMnfXCaBo2cs7c8Edoacbjcc2Z3js6jHf+Pz8FY6nb5udmBj0q4zl+a5PxbjWVgOQcA==",
"requires": {
"@babel/runtime": "^7.20.1",
"complex.js": "^2.1.1",
"decimal.js": "^10.4.2",
"escape-latex": "^1.2.0",
"fraction.js": "^4.2.0",
"javascript-natural-sort": "^0.7.1",
"seedrandom": "^3.0.5",
"tiny-emitter": "^2.1.0",
"typed-function": "^4.1.0"
},
"dependencies": {
"@babel/runtime": {
"version": "7.20.1",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.1.tgz",
"integrity": "sha512-mrzLkl6U9YLF8qpqI7TB82PESyEGjm/0Ly91jG575eVxMMlb8fYfOXFZIJ8XfLrJZQbm7dlKry2bJmXBUEkdFg==",
"requires": {
"regenerator-runtime": "^0.13.10"
}
},
"regenerator-runtime": {
"version": "0.13.10",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz",
"integrity": "sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw=="
}
}
},
"md5.js": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
@@ -13295,11 +13086,6 @@
"dev": true,
"optional": true
},
"oauth-sign": {
"version": "0.9.0",
"resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.9.0.tgz",
"integrity": "sha512-fexhUFFPTGV8ybAtSIGbV6gOkSv8UtRbDBnAyLQw4QPKkgNlsH2ByPGtMUqdWkos6YCRmAqViwgZrJc/mRDzZQ=="
},
"object-assign": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz",
@@ -13395,56 +13181,6 @@
"isobject": "^3.0.1"
}
},
"ocsp": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/ocsp/-/ocsp-1.2.0.tgz",
"integrity": "sha512-r4Q3oYKU+3b6iD4bn+5O2dQqctu8pFrJfWouUiKjiNXXjdr99lN/EaTVkFQevGlV/lKsomgtt/XRGB8xV8rq3Q==",
"requires": {
"asn1.js": "^4.8.0",
"asn1.js-rfc2560": "^4.0.0",
"asn1.js-rfc5280": "^2.0.0",
"async": "^1.5.2",
"simple-lru-cache": "0.0.2"
},
"dependencies": {
"asn1.js": {
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz",
"integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==",
"requires": {
"bn.js": "^4.0.0",
"inherits": "^2.0.1",
"minimalistic-assert": "^1.0.0"
}
},
"asn1.js-rfc2560": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/asn1.js-rfc2560/-/asn1.js-rfc2560-4.0.6.tgz",
"integrity": "sha512-ysf48ni+f/efNPilq4+ApbifUPcSW/xbDeQAh055I+grr2gXgNRQqHew7kkO70WSMQ2tEOURVwsK+dJqUNjIIg==",
"requires": {
"asn1.js-rfc5280": "^2.0.0"
}
},
"asn1.js-rfc5280": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/asn1.js-rfc5280/-/asn1.js-rfc5280-2.0.1.tgz",
"integrity": "sha512-1e2ypnvTbYD/GdxWK77tdLBahvo1fZUHlQJqAVUuZWdYj0rdjGcf2CWYUtbsyRYpYUMwMWLZFUtLxog8ZXTrcg==",
"requires": {
"asn1.js": "^4.5.0"
}
},
"async": {
"version": "1.5.2",
"resolved": "https://registry.npmjs.org/async/-/async-1.5.2.tgz",
"integrity": "sha512-nSVgobk4rv61R9PUSDtYt7mPVB2olxNR5RWJcAsH676/ef11bUZwvu7+RGYrYauVdDPcO519v68wRhXQtxsV9w=="
},
"bn.js": {
"version": "4.12.0",
"resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.0.tgz",
"integrity": "sha512-c98Bf3tPniI+scsdk237ku1Dc3ujXQTSgyiPUDEOe7tRkhrqridvh8klBv0HCEso1OLOYcHuCv/cS6DNxKH+ZA=="
}
}
},
"odbc": {
"version": "2.4.6",
"resolved": "https://registry.npmjs.org/odbc/-/odbc-2.4.6.tgz",
@@ -14099,11 +13835,6 @@
"integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==",
"dev": true
},
"performance-now": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/performance-now/-/performance-now-2.1.0.tgz",
"integrity": "sha512-7EAHlyLHI56VEIdK57uwHdHKIaAGbnXPiw0yWbarQZOKaKpvUIgW0jWRVLiatnM+XXlSwsanIBH/hzGMJulMow=="
},
"pg": {
"version": "8.8.0",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.8.0.tgz",
@@ -14959,74 +14690,6 @@
"integrity": "sha512-sL26E4+8Kec7bwpRjHlQvbNZcpnGroT3PA7ywsgH6GjzxAg4IGNlNalLoRC/JmTed7cMhyDbi44pWw1kMhDxlw==",
"dev": true
},
"request": {
"version": "2.88.2",
"resolved": "https://registry.npmjs.org/request/-/request-2.88.2.tgz",
"integrity": "sha512-MsvtOrfG9ZcrOwAW+Qi+F6HbD0CWXEh9ou77uOb7FM2WPhwT7smM833PzanhJLsgXjN89Ir6V2PczXNnMpwKhw==",
"requires": {
"aws-sign2": "~0.7.0",
"aws4": "^1.8.0",
"caseless": "~0.12.0",
"combined-stream": "~1.0.6",
"extend": "~3.0.2",
"forever-agent": "~0.6.1",
"form-data": "~2.3.2",
"har-validator": "~5.1.3",
"http-signature": "~1.2.0",
"is-typedarray": "~1.0.0",
"isstream": "~0.1.2",
"json-stringify-safe": "~5.0.1",
"mime-types": "~2.1.19",
"oauth-sign": "~0.9.0",
"performance-now": "^2.1.0",
"qs": "~6.5.2",
"safe-buffer": "^5.1.2",
"tough-cookie": "~2.5.0",
"tunnel-agent": "^0.6.0",
"uuid": "^3.3.2"
},
"dependencies": {
"form-data": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/form-data/-/form-data-2.3.3.tgz",
"integrity": "sha512-1lLKB2Mu3aGP1Q/2eCOx0fNbRMe7XdwktwOruhfqqd0rIJWwN4Dh+E3hrPSlDCXnSR7UtZ1N38rVXm+6+MEhJQ==",
"requires": {
"asynckit": "^0.4.0",
"combined-stream": "^1.0.6",
"mime-types": "^2.1.12"
}
},
"qs": {
"version": "6.5.3",
"resolved": "https://registry.npmjs.org/qs/-/qs-6.5.3.tgz",
"integrity": "sha512-qxXIEh4pCGfHICj1mAJQ2/2XVZkjCDTcEgfoSQxc/fYivUZxTkk7L3bDBJSoNrEzXI17oUO5Dp07ktqE5KzczA=="
},
"tough-cookie": {
"version": "2.5.0",
"resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz",
"integrity": "sha512-nlLsUzgm1kfLXSXfRZMc1KLAugd4hqJHDTvc2hDIwS3mZAfMEuMbc03SujMF+GEcpaX/qboeycw6iO8JwVv2+g==",
"requires": {
"psl": "^1.1.28",
"punycode": "^2.1.1"
}
},
"uuid": {
"version": "3.4.0",
"resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz",
"integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A=="
}
}
},
"requestretry": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/requestretry/-/requestretry-4.1.2.tgz",
"integrity": "sha512-N1WAp+8eOy8NfsVBChcSxNCKvPY1azOpliQ4Sby4WDe0HFEhdKywlNZeROMBQ+BI3Jpc0eNOT1KVFGREawtahA==",
"requires": {
"extend": "^3.0.2",
"lodash": "^4.17.15",
"when": "^3.7.7"
}
},
"require-directory": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz",
@@ -15271,6 +14934,11 @@
}
}
},
"seedrandom": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz",
"integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg=="
},
"seek-bzip": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/seek-bzip/-/seek-bzip-1.0.6.tgz",
@@ -16419,22 +16087,6 @@
"tar": "^6.1.11"
}
},
"sshpk": {
"version": "1.17.0",
"resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.17.0.tgz",
"integrity": "sha512-/9HIEs1ZXGhSPE8X6Ccm7Nam1z8KcoCqPdI7ecm1N33EzAetWahvQWVqLZtaZQ+IDKX4IyA2o0gBzqIMkAagHQ==",
"requires": {
"asn1": "~0.2.3",
"assert-plus": "^1.0.0",
"bcrypt-pbkdf": "^1.0.0",
"dashdash": "^1.12.0",
"ecc-jsbn": "~0.1.1",
"getpass": "^0.1.1",
"jsbn": "~0.1.0",
"safer-buffer": "^2.0.2",
"tweetnacl": "~0.14.0"
}
},
"ssri": {
"version": "8.0.1",
"resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz",
@@ -17432,6 +17084,11 @@
"next-tick": "1"
}
},
"tiny-emitter": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
},
"tmp": {
"version": "0.0.33",
"resolved": "https://registry.npmjs.org/tmp/-/tmp-0.0.33.tgz",
@@ -17709,11 +17366,6 @@
"safe-buffer": "^5.0.1"
}
},
"tweetnacl": {
"version": "0.14.5",
"resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.5.tgz",
"integrity": "sha512-KXXFFdAbFXY4geFIwoyNK+f5Z1b7swfXABfL7HXCmoIWMKU3dmS26672A4EeQtDzLKy7SXmfBu51JolvEKwtGA=="
},
"type": {
"version": "2.7.2",
"resolved": "https://registry.npmjs.org/type/-/type-2.7.2.tgz",
@@ -17750,6 +17402,11 @@
"mime-types": "~2.1.24"
}
},
"typed-function": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.1.0.tgz",
"integrity": "sha512-DGwUl6cioBW5gw2L+6SMupGwH/kZOqivy17E4nsh1JI9fKF87orMmlQx3KISQPmg3sfnOUGlwVkroosvgddrlg=="
},
"typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",
@@ -18033,6 +17690,7 @@
"version": "4.4.1",
"resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz",
"integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==",
"dev": true,
"requires": {
"punycode": "^2.1.0"
}
@@ -18225,23 +17883,6 @@
}
}
},
"verror": {
"version": "1.10.0",
"resolved": "https://registry.npmjs.org/verror/-/verror-1.10.0.tgz",
"integrity": "sha512-ZZKSmDAEFOijERBLkmYfJ+vmk3w+7hOLYDNkRCuRuMJGEmqYNCNLyBBFwWKVMhfwaEF3WOd0Zlw86U/WC/+nYw==",
"requires": {
"assert-plus": "^1.0.0",
"core-util-is": "1.0.2",
"extsprintf": "^1.2.0"
},
"dependencies": {
"core-util-is": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz",
"integrity": "sha512-3lqz5YjWTYnW6dlDa5TLaTCcShfar1e40rmcJVwCBJC6mWlFuj0eCHIElmG1g5kyuJ/GD+8Wn4FFCcz4gJPfaQ=="
}
}
},
"vm-browserify": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",
@@ -18342,11 +17983,6 @@
"webidl-conversions": "^3.0.0"
}
},
"when": {
"version": "3.7.8",
"resolved": "https://registry.npmjs.org/when/-/when-3.7.8.tgz",
"integrity": "sha512-5cZ7mecD3eYcMiCH4wtRPA5iFJZ50BJYDfckI5RRpQiktMiYTcn0ccLTZOvcbBume+1304fQztxeNzNS9Gvrnw=="
},
"which": {
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz",

View File

@@ -50,7 +50,7 @@
"knex-bigquery": "^2.0.3",
"knex-db2": "^1.0.0",
"knex-schema-inspector": "^2.0.4",
"knex-snowflake-dialect": "^1.0.1",
"mathjs": "^11.3.3",
"mustache": "^4.2.0",
"mysql2": "^2.3.3",
"nest-winston": "^1.7.0",
@@ -107,7 +107,8 @@
"json",
"ts"
],
"rootDir": "src",
"testTimeout": 20000,
"rootDir": ".",
"testRegex": ".*\\.spec\\.ts$",
"transform": {
"^.+\\.(t|j)s$": "ts-jest"

Binary file not shown.

View File

@@ -7,6 +7,9 @@ import { Database } from '../database/entities/database.entity';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { ResponseStatus } from '../common/enum/response-status.enum';
import { SnowflakeDialect } from './knex-dialects/snowflake';
const { BigQueryClient } = require('knex-bigquery');
const knexConnections = new Map<number, Knex>();
@@ -44,12 +47,17 @@ export class ConnectionService {
/**
* Knex 객체 가져오기 - 만약 없으면 가져오기
* @param id
* @param databaseInfo
*/
async getKnex(id: number): Promise<Knex> {
if (!this.hasKnex(id)) {
const one = await this.databaseRepository.findOne({ where: { id: id } });
one.connectionConfig = JSON.parse(one.connectionConfig);
const knexConfig = one.connectionConfig;
if (knexConfig['client'] == 'bigquery') {
knexConfig['client'] = BigQueryClient;
} else if (knexConfig['client'] == 'snowflake') {
knexConfig['client'] = SnowflakeDialect;
}
this.addKnex(id, one.connectionConfig as Knex.Config);
}
return knexConnections.get(id);
@@ -60,12 +68,30 @@ export class ConnectionService {
* @param createDatabaseDto
*/
async testConnection(createDatabaseDto: CreateDatabaseDto) {
let engine: any = createDatabaseDto.engine;
switch (createDatabaseDto.engine) {
case 'bigquery':
engine = BigQueryClient;
break;
case 'snowflake':
engine = SnowflakeDialect;
break;
}
if (createDatabaseDto.engine === 'cockroachdb') {
const connectioninfo = createDatabaseDto.connectionConfig;
const cockroach_url = `postgresql://${connectioninfo['user']}:${connectioninfo['password']}@${connectioninfo['host']}:${connectioninfo['port']}/${connectioninfo['database']}?sslmode=verify-full&options=--cluster%3Dvanillameta-cockroach-3010`;
connectioninfo['connectionString'] = cockroach_url;
}
const connectionConfig = {
client: createDatabaseDto.engine,
client: engine,
connection: createDatabaseDto.connectionConfig,
useNullAsDefault: true,
};
createDatabaseDto.connectionConfig = JSON.stringify(connectionConfig);
// createDatabaseDto.connectionConfig = JSON.stringify(connectionConfig);
// console.log(createDatabaseDto)
let _knex: Knex;
let returnObj = {};
try {
@@ -76,8 +102,10 @@ export class ConnectionService {
return { status: ResponseStatus.ERROR, message: 'knex not connected' };
}
const testQuery = createDatabaseDto.engine == 'oracledb' ? 'SELECT 1 FROM DUAL' : 'SELECT 1';
try {
await _knex.raw('SELECT 1');
await _knex.raw(testQuery);
returnObj = { status: ResponseStatus.SUCCESS, data: { message: 'success' } };
} catch (e) {
console.log(e);
@@ -98,108 +126,120 @@ export class ConnectionService {
let datas = [];
const fields = [];
const resultObj = { status: ResponseStatus.SUCCESS, message: 'success', datas: [], fields: [] };
const resultObj = { status: null, message: null, datas: [], fields: [] };
try {
const queryRes = await knex.raw(queryExecuteDto.query);
switch (knex.client.config.client) {
case 'mysql2':
if (queryRes && queryRes[0].length > 0) {
datas = queryRes[0];
const tempFields = queryRes[1];
tempFields.map(field => {
const fieldInfo = {
columnName: field.name,
columnType: FieldTypeUtil.mysqlFieldType(field.columnType),
};
fields.push(fieldInfo);
});
// bigquery, snowflake
if (typeof knex.client.config.client === 'function') {
switch (knex.client.config.client.name) {
case 'SnowflakeDialect':
if (queryRes && queryRes.rows && queryRes.rows.length > 0) {
datas = queryRes.rows;
const tempFields = Object.keys(queryRes.rows[0]);
tempFields.map(field => {
const length = [];
const maxCnt = queryRes.rows.length > 100 ? 100 : queryRes.rows.length;
for (let i = 0; i < maxCnt; i++) {
length.push(queryRes.rows[i][field]);
}
const fieldInfo = {
columnName: field,
columnType: FieldTypeUtil.FieldType(length),
};
fields.push(fieldInfo);
});
}
break;
}
case 'BigQueryClient':
if (queryRes && queryRes.length > 0) {
datas = queryRes;
const tempFields = Object.keys(queryRes[0]);
case 'sqlite':
if (queryRes && queryRes.length > 0) {
datas = queryRes;
const tempFields = Object.keys(queryRes[0]);
tempFields.map(field => {
const length = [];
for(let i = 0 ; i < 100; i ++){
length.push(queryRes[i][field])
}
const fieldInfo = {
columnName: field,
columnType: FieldTypeUtil.FieldType(length),
};
fields.push(fieldInfo);
});
tempFields.map(field => {
const length = [];
const maxCnt = queryRes.length > 100 ? 100 : queryRes.length;
for (let i = 0; i < maxCnt; i++) {
length.push(queryRes[i][field]);
}
const fieldInfo = {
columnName: field,
columnType: FieldTypeUtil.FieldType(length),
};
fields.push(fieldInfo);
});
}
break;
}
case 'pg':
if (queryRes && queryRes.rows.length > 0) {
datas = queryRes.rows;
const tempFields = queryRes.fields;
tempFields.map(field => {
const length = [];
for(let i = 0 ; i < 100; i ++){
length.push(queryRes.rows[i][field.name])
}
const fieldInfo = {
columnName: field.name,
columnType: FieldTypeUtil.FieldType(length),
};
fields.push(fieldInfo);
});
}
} else {
switch (knex.client.config.client) {
case 'mysql2':
if (queryRes && queryRes[0].length > 0) {
datas = queryRes[0];
const tempFields = queryRes[1];
tempFields.map(field => {
const fieldInfo = {
columnName: field.name,
columnType: FieldTypeUtil.mysqlFieldType(field.columnType),
};
fields.push(fieldInfo);
});
}
break;
}
case 'Db2Dialect':
if (queryRes && queryRes.length > 0) {
datas = queryRes;
// for (let i = 0; i < Object.keys(queryRes[0]).length; i++) {
// console.log(Object.keys[i]);
// }
const tempFields = queryRes;
tempFields.map(field => {
const fieldInfo = {
columnName: field.name,
columnLength: field.length,
columnType: FieldTypeUtil.mysqlFieldType(field.type),
};
fields.push(fieldInfo);
});
case 'cockroachdb':
case 'pg':
if (queryRes && queryRes.rows && queryRes.rows.length > 0) {
datas = queryRes.rows;
const tempFields = queryRes.fields;
tempFields.map(field => {
const length = [];
const maxCnt = queryRes.rows.length > 100 ? 100 : queryRes.rows.length;
for (let i = 0; i < maxCnt; i++) {
length.push(queryRes.rows[i][field.name]);
}
const fieldInfo = {
columnName: field.name,
columnType: FieldTypeUtil.FieldType(length),
};
fields.push(fieldInfo);
});
}
break;
}
case 'mssql':
if (queryRes && queryRes.length > 0) {
datas = queryRes;
const tempFields = Object.keys(queryRes[0]);
tempFields.map(field => {
const fieldInfo = {
columnName: field,
columnType: FieldTypeUtil.mysqlFieldType(field),
};
fields.push(fieldInfo);
});
}
break;
// case 'sqlite3':
// case 'mssql':
// case 'oracledb':
default:
if (queryRes && queryRes.length > 0) {
datas = queryRes;
const tempFields = Object.keys(queryRes[0]);
tempFields.map(field => {
const length = [];
const maxCnt = queryRes.length > 100 ? 100 : queryRes.length;
for (let i = 0; i < maxCnt; i++) {
length.push(queryRes[i][field]);
}
const fieldInfo = {
columnName: field,
columnType: FieldTypeUtil.FieldType(length),
};
fields.push(fieldInfo);
});
}
break;
}
}
resultObj.status = ResponseStatus.SUCCESS;
resultObj.message = 'success';
resultObj.datas = datas;
resultObj.fields = fields;
} catch (e) {
resultObj.status = ResponseStatus.ERROR;
resultObj.message = e.sqlMessage;
if (e.sqlMessage) resultObj.message = e.sqlMessage;
else if (e.message) resultObj.message = e.message; // bigquery
console.log(e);
console.log(e.sqlMessage);
}
return resultObj;

View File

@@ -0,0 +1,160 @@
// import { Knex, knex } from 'knex';
// import { Database } from 'ibm_db';
// import Client = knex.Client;
// const Promise = require('bluebird');
// const Client = require('knex/lib/client');
//
// class DB2Client extends Client {
// constructor(config) {
// super(config);
// }
//
// get dialect() {
// return 'ibm_db';
// }
//
// get driverName() {
// return 'ibm_db';
// }
//
// get canCancelQuery() {
// return true;
// }
//
// _driver() {
// return Promise.promisifyAll(require(this.driverName));
// }
//
// // transaction() {
// // return new Transaction(this, ...arguments);
// // }
//
// wrapIdentifierImpl(value) {
// // override default wrapper ("). we don't want to use it since
// // it makes identifiers case-sensitive in DB2
// return value;
// }
//
// // printDebug(message) {
// // if (process.env.DEBUG === 1) {
// // this.logger.log(message);
// // }
// // }
//
// // Get a raw connection, called by the pool manager whenever a new
// // connection needs to be added to the pool.
// acquireRawConnection() {
// // this.printDebug('acquiring raw connection.');
// const connectionConfig = this.config.connection;
// return new Promise((resolve, reject) => {
// this.driver.open(this._getConnectionString(connectionConfig), (err, connection) => {
// if (err) {
// return reject(err);
// }
//
// return resolve(connection);
// });
// });
// }
//
// // Used to explicitly close a connection, called internally by the pool manager
// // when a connection times out or the pool is shutdown.
// destroyRawConnection(connection) {
// // this.printDebug('destroying raw connection');
//
// return connection.closeAsync();
// }
//
// validateConnection(connection) {
// return Promise.resolve(connection.connected);
// }
//
// _stream(connection, obj, stream, options) {
// this._stream(connection, obj, stream, options);
// throw new Error('Not yet implemented');
// }
//
// _getConnectionString(connectionConfig = {}) {
// const connectionStringParams = connectionConfig.connectionStringParams || {};
// const connectionStringExtension = Object.keys(connectionStringParams).reduce((result, key) => {
// const value = connectionStringParams[key];
// return `${result}${key}=${value};`;
// }, '');
//
// const connectionString = `${
// `DRIVER=${connectionConfig.driver};SYSTEM=${connectionConfig.host};HOSTNAME=${connectionConfig.host};` +
// `PORT=${connectionConfig.port};DATABASE=${connectionConfig.database};` +
// `UID=${connectionConfig.user};PWD=${connectionConfig.password};`
// }${connectionStringExtension}`;
//
// return connectionString;
// }
//
// // Runs the query on the specified connection, providing the bindings
// // and any other necessary prep work.
// _query(connection, obj) {
// // TODO: verify correctness
// if (!obj || typeof obj === 'string') obj = { sql: obj };
//
// const method = (obj.method !== 'raw' ? obj.method : obj.sql.split(' ')[0]).toLowerCase();
//
// obj.sqlMethod = method;
//
// // Different functions are used since query() doesn't return # of rows affected,
// // which is needed for queries that modify the database
// if (method === 'select' || method === 'first' || method === 'pluck') {
// return connection.queryAsync(obj.sql, obj.bindings).then(rows => {
// obj.response = {
// rows,
// rowCount: rows.length,
// };
//
// return obj;
// });
// }
//
// return connection
// .prepareAsync(obj.sql)
// .then(statement => statement.executeNonQueryAsync(obj.bindings))
// .then(numRowsAffected => {
// obj.response = {
// rowCount: numRowsAffected,
// };
//
// return obj;
// });
// }
//
// // Process / normalize the response as returned from the query
// processResponse(obj, runner) {
// // TODO: verify correctness
//
// if (obj === null) return null;
//
// const resp = obj.response;
// const method = obj.sqlMethod;
// const { rows } = resp;
//
// if (obj.output) return obj.output.call(runner, resp);
//
// switch (method) {
// case 'select':
// case 'pluck':
// case 'first': {
// if (method === 'pluck') return rows.map(obj.pluck);
// return method === 'first' ? rows[0] : rows;
// }
// case 'insert':
// case 'del':
// case 'delete':
// case 'update':
// case 'counter':
// return resp.rowCount;
// default:
// return resp;
// }
// }
// }
//
// module.exports = { DB2Client };

View File

@@ -0,0 +1,17 @@
// import Transaction from 'knex/lib/execution/transaction';
//
// class TransactionDb2 extends Transaction {
// begin(conn) {
// return conn.beginTransactionAsync().then(this._resolver).catch(this._rejecter);
// }
//
// commit(conn, value) {
// this._completed = true;
// return conn
// .commitTransactionAsync()
// .then(() => this._resolver(value))
// .catch(this._rejecter);
// }
// }
//
// module.exports = TransactionDb2;

View File

@@ -0,0 +1,84 @@
// // import Client from "knex/lib/client";
//
// import { Knex, knex } from 'knex';
// import { Database } from 'ibm_db';
// import Client = knex.Client;
//
// export class IbmDbClient extends Client {
// constructor(config) {
// super(config);
// this.dialect = 'ibm_db';
// this.driverName = 'ibm_db';
// this.canCancelQuery = true;
// }
//
// _driver() {
// return new Database();
// }
//
// acquireRawConnection() {
// return Promise.resolve({
// driver: this.driver,
// job: null,
// });
// }
//
// // validateConnection(connection) {
// // return (
// // connection &&
// // !connection._fatalError &&
// // !connection._protocolError &&
// // !connection._closing &&
// // !connection.stream.destroyed
// // );
// // }
//
// destroyRawConnection(connection) {
// return this.cancelJob(connection);
// }
//
// wrapIdentifier(value) {
// return value !== '*' ? `\`${value}\`` : '*';
// }
//
// cancelJob(connection) {
// if (connection.job === null) {
// return Promise.resolve();
// }
// const cancelJobRequest = connection.job.cancel();
// connection.job = null;
// return cancelJobRequest;
// }
//
// _query(connection, obj) {
// const queryConfig = {
// ...obj.options,
// query: obj.sql,
// params: obj.bindings,
// };
//
// return this.createJob(connection, queryConfig)
// .then(connection => this.getJobResults(connection, obj))
// .catch(err => {
// this.cancelJob(connection);
// throw err;
// });
// }
//
// createJob(connection, queryConfig) {
// return Promise.resolve(
// connection.driver.createQueryJob(queryConfig).then(res => {
// connection.job = res[0];
// return connection;
// }),
// );
// }
//
// getJobResults(connection, obj) {
// return connection.job.getQueryResults({ autoPaginate: false }).then(res => {
// obj.response = res[0];
// connection.job = null;
// return obj;
// });
// }
// }

View File

@@ -0,0 +1,238 @@
// import * as Promise from 'bluebird';
import { Knex, knex } from 'knex';
import { defer, fromPairs, isArray, toPairs } from 'lodash';
import * as ColumnBuilder from 'knex/lib/schema/columnbuilder';
import * as ColumnCompiler_MySQL from 'knex/lib/dialects/mysql/schema/mysql-columncompiler';
import * as Transaction from 'knex/lib/execution/transaction';
import { promisify } from 'util';
export class SnowflakeDialect extends knex.Client {
constructor(
config = {
dialect: 'snowflake',
driverName: 'snowflake-sdk',
} as any,
) {
if (config.connection) {
if (config.connection.user && !config.connection.username) {
config.connection.username = config.connection.user;
}
if (config.connection.host) {
const [account, region] = config.connection.host.split('.');
if (!config.connection.account) {
config.connection.account = account;
}
if (!config.connection.region) {
config.connection.region = region;
}
}
}
super(config);
}
transaction(container: any, config: any, outerTx: any): Knex.Transaction {
const transax = new Transaction(this, container, config, outerTx);
transax.savepoint = (conn: any) => {
// @ts-ignore
transax.trxClient.logger('Snowflake does not support savepoints.');
};
transax.release = (conn: any, value: any) => {
// @ts-ignore
transax.trxClient.logger('Snowflake does not support savepoints.');
};
transax.rollbackTo = (conn: any, error: any) => {
// @ts-ignore
this.trxClient.logger('Snowflake does not support savepoints.');
};
return transax;
}
// @ts-ignore
// queryCompiler(builder: any, formatter: any) {
// return new QueryCompiler(this, builder, formatter);
// }
columnBuilder(tableBuilder: any, type: any, args: any) {
// ColumnBuilder methods are created at runtime, so that it does not play well with TypeScript.
// So instead of extending ColumnBuilder, we override methods at runtime here
const columnBuilder = new ColumnBuilder(this, tableBuilder, type, args);
columnBuilder.primary = (constraintName?: string | undefined): Knex.ColumnBuilder => {
// @ts-ignore
columnBuilder.notNullable();
return columnBuilder;
};
columnBuilder.index = (indexName?: string | undefined): Knex.ColumnBuilder => {
// @ts-ignore
columnBuilder.client.logger.warn('Snowflake does not support the creation of indexes.');
return columnBuilder;
};
return columnBuilder;
}
columnCompiler(tableCompiler: any, columnBuilder: any) {
// ColumnCompiler methods are created at runtime, so that it does not play well with TypeScript.
// So instead of extending ColumnCompiler, we override methods at runtime here
const columnCompiler = new ColumnCompiler_MySQL(
this,
tableCompiler.tableBuilder,
columnBuilder,
);
columnCompiler.increments = 'int not null autoincrement primary key';
columnCompiler.bigincrements = 'bigint not null autoincrement primary key';
columnCompiler.mediumint = (colName: string) => 'integer';
columnCompiler.decimal = (colName: string, precision?: number, scale?: number) => {
if (precision) {
return ColumnCompiler_MySQL.prototype.decimal(colName, precision, scale);
}
return 'decimal';
};
columnCompiler.double = (colName: string, precision?: number, scale?: number) => {
if (precision) {
return ColumnCompiler_MySQL.prototype.decimal(colName, precision, scale);
}
return 'double';
};
columnCompiler.enu = (colName: string, values: string[]) => 'varchar';
columnCompiler.json = columnCompiler.jsonb = (colName: string) => 'variant';
return columnCompiler;
}
// tableCompiler(tableBuilder: any) {
// return new TableCompiler(this, tableBuilder);
// }
//
// schemaCompiler(builder: any) {
// return new SchemaCompiler(this, builder);
// }
_driver() {
const Snowflake = require('snowflake-sdk');
return Snowflake;
}
// Get a raw connection, called by the `pool` whenever a new
// connection needs to be added to the pool.
acquireRawConnection() {
return new Promise((resolver, rejecter) => {
// @ts-ignore
const connection = this.driver.createConnection(this.connectionSettings);
connection.on('error', err => {
connection.__knex__disposed = err;
});
connection.connect(err => {
if (err) {
// if connection is rejected, remove listener that was registered above...
connection.removeAllListeners();
return rejecter(err);
}
resolver(connection);
});
});
}
// Used to explicitly close a connection, called internally by the pool
// when a connection times out or the pool is shutdown.
async destroyRawConnection(connection): Promise<void> {
try {
const end = promisify(cb => connection.end(cb));
await end();
} catch (err) {
connection.__knex__disposed = err;
} finally {
// see discussion https://github.com/knex/knex/pull/3483
defer(() => connection.removeAllListeners());
}
}
async validateConnection(connection: any): Promise<boolean> {
if (connection) {
return true;
}
return false;
}
// Runs the query on the specified connection, providing the bindings
// and any other necessary prep work.
_query(connection: any, obj: any) {
if (!obj || typeof obj === 'string') obj = { sql: obj };
return new Promise((resolver: any, rejecter: any) => {
if (!obj.sql) {
resolver();
return;
}
const queryOptions = {
sqlText: obj.sql,
binds: obj.bindings,
complete(err: any, statement: any, rows: any) {
if (err) return rejecter(err);
obj.response = { rows, statement };
resolver(obj);
},
...obj.options,
};
connection.execute(queryOptions);
});
}
// Ensures the response is returned in the same format as other clients.
processResponse(obj: any, runner: any) {
const resp = obj.response;
if (obj.output) return obj.output.call(runner, resp);
if (obj.method === 'raw') return resp;
if (obj.method === 'select') {
// if (obj.method === 'first') return resp.rows[0];
// if (obj.method === 'pluck') return map(resp.rows, obj.pluck);
return resp.rows;
}
if (obj.method === 'insert' || obj.method === 'update' || obj.method === 'delete') {
if (resp.rows) {
const method = obj.method === 'insert' ? 'inserte' : obj.method;
return resp.rows.reduce((count, row) => count + row[`number of rows ${method}d`], 0);
}
return resp;
}
if (resp.statement && resp.rows) {
return resp.rows;
}
return resp;
}
postProcessResponse(result, queryContext) {
if (this.config.postProcessResponse) {
return this.config.postProcessResponse(result, queryContext);
}
// Snowflake returns column names in uppercase, convert to lowercase
// (to conform with knex, e.g. schema migrations)
const lowercaseAttrs = (row: any) => {
return fromPairs(toPairs(row).map(([key, value]) => [key.toLowerCase(), value]));
};
if (result.rows) {
return {
...result,
rows: result.rows.map(lowercaseAttrs),
};
} else if (isArray(result)) {
return result.map(lowercaseAttrs);
}
return result;
}
customWrapIdentifier(value, origImpl, queryContext) {
if (this.config.wrapIdentifier) {
return this.config.wrapIdentifier(value, origImpl, queryContext);
} else if (!value.startsWith('"')) {
return origImpl(value.toUpperCase());
}
return origImpl;
}
}
Object.assign(SnowflakeDialect.prototype, {
// The "dialect", for reference elsewhere.
driverName: 'snowflake-sdk',
});

View File

@@ -0,0 +1,20 @@
// @ts-ignore
import * as QueryCompiler_MySQL from 'knex/lib/dialects/mysql/query/mysql-querycompiler';
export class QueryCompiler extends QueryCompiler_MySQL {
constructor(client: any, builder: any, formatter: any) {
super(client, builder, formatter);
}
forUpdate() {
// @ts-ignore
this.client.logger.warn('table lock is not supported by snowflake dialect');
return '';
}
forShare() {
// @ts-ignore
this.client.logger.warn('lock for share is not supported by snowflake dialect');
return '';
}
}

View File

@@ -0,0 +1,30 @@
// @ts-ignore
import * as SchemaCompiler_MySQL from "knex/lib/dialects/mysql/schema/mysql-compiler";
export class SchemaCompiler extends SchemaCompiler_MySQL {
constructor(client: any, builder: any) {
super(client, builder);
}
// Check whether a table exists on the query.
hasTable(tableName: string) {
const [ schema, table ] = tableName.includes(".") ? tableName.split(".") : [undefined, tableName];
let sql = 'select * from information_schema.tables where table_name = ?';
const bindings = [table.toUpperCase()];
if (schema) {
sql += ' and table_schema = ?';
bindings.push(schema.toUpperCase());
} else {
sql += ' and table_schema = current_schema()';
}
// @ts-ignore
this.pushQuery({
sql,
bindings,
output: (resp) => resp.rows.length > 0
});
}
}

View File

@@ -0,0 +1,18 @@
// @ts-ignore
import * as TableCompiler_MySQL from "knex/lib/dialects/mysql/schema/mysql-tablecompiler";
export class TableCompiler extends TableCompiler_MySQL {
constructor(client: any, builder: any) {
super(client, builder);
}
index(columns, indexName, indexType) {
// @ts-ignore
this.client.logger.warn('Snowflake does not support the creation of indexes.');
};
dropIndex(columns, indexName) {
// @ts-ignore
this.client.logger.warn('Snowflake does not support the deletion of indexes.');
};
}

View File

@@ -0,0 +1,2 @@
export * from "./SchemaCompiler";
export * from "./TableCompiler";

View File

@@ -0,0 +1 @@
declare module "snowflake-sdk";

View File

@@ -5,7 +5,7 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { Dashboard } from './entities/dashboard.entity';
import { DashboardWidget } from './dashboard-widget/entities/dashboard-widget.entity';
import { DashboardWidgetService } from './dashboard-widget/dashboard-widget.service';
import { Widget } from 'src/widget/entities/widget.entity';
import { Widget } from '../widget/entities/widget.entity';
import { Component } from '../component/entities/component.entity';
@Module({

View File

@@ -42,6 +42,7 @@ export class DashboardService {
const find_all = await this.dashboardRepository.find({
order: {
updatedAt: 'desc',
title: 'asc',
},
});
find_all.forEach(el => {

View File

@@ -24,12 +24,24 @@ export class DatabaseController {
@Get('/data')
async findData(
@Query('datasetType') datasetType: DatasetType,
@Query('datasetId') datasetId: number,
@Query('databaseId') databaseId: number,
@Query('datasetId') datasetId?: number,
@Query('tableName') tableName?: string,
) {
const res = await this.databaseService.findData(datasetType, datasetId);
const res = await this.databaseService.findData(datasetType, databaseId, datasetId, tableName);
return res;
}
/**
* 데이터베이스 연결정보 단순조회
* @param id
*/
@Get('/info/:id')
async findOneInfo(@Param('id') id: string) {
const databaseInfo = await this.databaseService.findOneInfo(+id);
return databaseInfo;
}
/**
* 데이터베이스 생성 ( 데이터소스 생성)
* @param createDatabaseDto

View File

@@ -10,8 +10,8 @@ import { ResponseStatus } from '../common/enum/response-status.enum';
import { DatasetType } from '../common/enum/dataset-type.enum';
import { TableQuery } from '../widget/tabel-query/entity/table-query.entity';
import { QueryExecuteDto } from './dto/query-execute.dto';
import {DatabaseType} from "./entities/database_type.entity";
import {YesNo} from "../common/enum/yn.enum";
import { DatabaseType } from './entities/database_type.entity';
import { YesNo } from '../common/enum/yn.enum';
@Injectable()
export class DatabaseService {
@@ -26,8 +26,8 @@ export class DatabaseService {
/**
* database type 목록 조회
*/
async findTypeList(){
const result = await this.databaseTypeRepository.find({where:{useYn:YesNo.YES}});
async findTypeList() {
const result = await this.databaseTypeRepository.find({ where: { useYn: YesNo.YES } });
return { status: ResponseStatus.SUCCESS, data: result };
}
@@ -43,6 +43,12 @@ export class DatabaseService {
connection: databaseDto.connectionConfig,
useNullAsDefault: true,
};
if (connectionConfig.client === 'cockroachdb') {
const connectioninfo = connectionConfig.connection;
const cockroach_url = `postgresql://${connectioninfo['user']}:${connectioninfo['password']}@${connectioninfo['host']}:${connectioninfo['port']}/${connectioninfo['database']}?sslmode=verify-full&options=--cluster%3Dvanillameta-cockroach-3010`;
connectioninfo['connectionString'] = cockroach_url;
}
databaseDto.connectionConfig = JSON.stringify(connectionConfig);
databaseDto.timezone = 'Asia/Seoul';
@@ -72,24 +78,76 @@ export class DatabaseService {
databaseInfo.connectionConfig = JSON.parse(databaseInfo.connectionConfig).connection;
// table 정보 조회
const tablesInfo = await this.connectionService.executeQuery({ id: +id, query: 'show tables' });
let selectTableQuery;
switch (databaseInfo.engine) {
case 'mysql2':
selectTableQuery = 'show tables';
break;
case 'pg':
selectTableQuery = `SELECT table_name FROM information_schema.tables WHERE table_type = 'BASE TABLE' and table_schema not in ('information_schema', 'pg_catalog', 'pg_internal')`;
break;
case 'sqlite3':
selectTableQuery = `SELECT tbl_name FROM sqlite_master WHERE type = 'table'`;
break;
case 'mssql':
selectTableQuery = 'SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES';
break;
case 'bigquery':
selectTableQuery = `select table_id from ${databaseInfo.connectionConfig['schema']}.__TABLES__`;
break;
case 'oracledb':
selectTableQuery = 'SELECT table_name FROM user_tables ORDER BY table_name';
break;
case 'snowflake':
selectTableQuery = `select table_name from information_schema.tables where table_type = 'BASE TABLE'`;
break;
case 'cockroachdb':
selectTableQuery = `SELECT TABLE_NAME FROM information_schema.tables WHERE table_type = 'BASE TABLE'`;
break;
default:
selectTableQuery = 'show tables';
break;
}
const tablesInfo = await this.connectionService.executeQuery({
id: +id,
query: selectTableQuery,
});
const tables = [];
if (tablesInfo && tablesInfo.datas.length > 0) {
tablesInfo.datas.map(tableObj => {
tables.push({ id: Object.values(tableObj)[0], tableName: Object.values(tableObj)[0], databaseId: id, datasetType:DatasetType.TABLE });
tables.push({
id: Object.values(tableObj)[0],
tableName: Object.values(tableObj)[0],
databaseId: id,
datasetType: DatasetType.TABLE,
});
});
} else if (tablesInfo && tablesInfo.status === ResponseStatus.ERROR) {
return tablesInfo;
}
// dataset 정보 조회
const tempDatasets = await this.datasetRepository.find({ where: { databaseId: id } });
const datasets = [];
tempDatasets.map(item => {
datasets.push(Object.assign({datasetType:DatasetType.DATASET}, item));
})
datasets.push(Object.assign({ datasetType: DatasetType.DATASET }, item));
});
return { status: ResponseStatus.SUCCESS, data: { databaseInfo, tables, datasets } };
}
/**
* database 정보 단순 조회
* @param id
*/
async findOneInfo(id: number): Promise<any> {
// 연동 db 정보
const databaseInfo = await this.databaseRepository.findOne({ where: { id } });
databaseInfo.connectionConfig = JSON.parse(databaseInfo.connectionConfig).connection;
return { status: ResponseStatus.SUCCESS, data: { databaseInfo } };
}
/**
* db config 정보 단순 조회
* @param id
@@ -109,13 +167,26 @@ export class DatabaseService {
message: `조건에 맞는 데이터베이스를 찾지 못했습니다. id:${id}`,
};
if (updateDatabaseDto.engine === 'cockroachdb') {
const connectioninfo = updateDatabaseDto.connectionConfig;
const cockroach_url = `postgresql://${connectioninfo['user']}:${connectioninfo['password']}@${connectioninfo['host']}:${connectioninfo['port']}/${connectioninfo['database']}?sslmode=verify-full&options=--cluster%3Dvanillameta-cockroach-3010`;
connectioninfo['connectionString'] = cockroach_url;
}
const connectionConfig = {
client: one.engine,
connection: Object(one.connectionConfig).connection,
connection: updateDatabaseDto.connectionConfig,
useNullAsDefault: true,
};
updateDatabaseDto.connectionConfig = JSON.stringify(connectionConfig);
// const connectionConfig = {
// client: one.engine,
// connection: Object(one.connectionConfig).connection,
// useNullAsDefault: true,
// };
// updateDatabaseDto.connectionConfig = JSON.stringify(connectionConfig);
const saveResult = await this.databaseRepository.update(
{ id },
{ name: updateDatabaseDto.name, connectionConfig: updateDatabaseDto.connectionConfig },
@@ -135,21 +206,57 @@ export class DatabaseService {
status: ResponseStatus.ERROR,
message: `조건에 맞는 데이터베이스를 찾지 못했습니다. id:${id}`,
};
// 연관된 widget 제거
const deletedWidget = await this.datasetRepository.query(
`delete from widget
where ((datasetType = 'DATASET' and datasetId in (select id from dataset where databaseId = ?)) or
(datasetType = 'TABLE' and datasetId in (select id from table_query where databaseId = ?)))`,
[id, id],
);
// table query 삭제
await this.tableQueryRepository.delete({ databaseId: id });
// dataset 삭제
const deletedDataset = await this.datasetRepository.delete({ databaseId: id });
// database 삭제
await this.databaseRepository.remove(one);
return { status: ResponseStatus.SUCCESS, data: { message: `${id} 삭제 완료` } };
return {
status: ResponseStatus.SUCCESS,
data: {
message: `${deletedWidget.affected}개의 widget, ${deletedDataset.affected}개의 dataset, databse [${one.name}] 삭제 완료`,
},
};
}
/**
* 데이터 조회
* @param datasetType
* @param databaseId
* @param datasetId
* @param tableName
*/
async findData(datasetType: DatasetType, datasetId: number) {
if (!datasetType || !datasetId)
async findData(
datasetType: DatasetType,
databaseId: number,
datasetId?: number,
tableName?: string,
) {
if (datasetType === DatasetType.DATASET && datasetId === undefined) {
return {
status: ResponseStatus.ERROR,
message: 'datasetType, datasetId 필수 입력 param 입니다',
message: 'DATASET의 경우, datasetId 필수 입력 사항입니다.',
};
} else if (
datasetType === DatasetType.TABLE &&
tableName === undefined &&
datasetId === undefined
) {
return {
status: ResponseStatus.ERROR,
message: 'TABLE의 경우, tableName이나 datasetId 둘 중 하나는 입력해야합니다.',
};
}
const queryExecuteDto = new QueryExecuteDto();
if (datasetType === DatasetType.DATASET) {
@@ -157,14 +264,35 @@ export class DatabaseService {
queryExecuteDto.id = datasetItem.databaseId;
queryExecuteDto.query = datasetItem.query;
} else if (datasetType === DatasetType.TABLE) {
const datasetItem = await this.tableQueryRepository.findOne({ where: { id: datasetId } });
queryExecuteDto.id = datasetItem.databaseId;
queryExecuteDto.query = datasetItem.query;
if (datasetId != undefined) {
const datasetItem = await this.tableQueryRepository.findOne({ where: { id: datasetId } });
queryExecuteDto.id = datasetItem.databaseId;
queryExecuteDto.query = datasetItem.query;
} else {
const databaseOne = await this.databaseRepository.findOne({ where: { id: databaseId } });
let selectQuery;
switch (databaseOne.type) {
case 'bigquery':
const schemaName = JSON.parse(databaseOne.connectionConfig).connection.schema;
selectQuery = `SELECT * FROM ${schemaName}.${tableName}`;
break;
case 'oracle':
selectQuery = `SELECT * FROM "${tableName}"`;
break;
default:
selectQuery = `SELECT * FROM ${tableName}`;
break;
}
queryExecuteDto.id = databaseId;
queryExecuteDto.query = selectQuery;
}
}
const queryResult = await this.connectionService.executeQuery(queryExecuteDto);
return {
status: queryResult.status,
data: { datas: queryResult.datas, fields: queryResult.fields },
};
if (queryResult.status === 'ERROR') return queryResult;
else
return {
status: queryResult.status,
data: { datas: queryResult.datas, fields: queryResult.fields },
};
}
}

View File

@@ -34,6 +34,14 @@ export class CreateDatabaseDto {
})
engine: string;
@IsString()
@IsOptional()
@ApiProperty({
example: 'mysql',
description: '데이터베이스 구분',
})
type: string;
@IsString()
@IsOptional()
@ApiProperty({

View File

@@ -16,9 +16,12 @@ export class Database extends BaseEntity {
@Column({ type: 'text', comment: '속성' })
connectionConfig: string; // 기타 속성 json으로 .. host, schema, filePath...
@Column({ length: 100, comment: '데이터베이스 구분' })
@Column({ length: 100, comment: '데이터베이스 엔진' })
engine: string;
@Column({ length: 100, comment: '데이터베이스 구분' })
type: string;
@Column({ length: 100, comment: '타임존', nullable: true })
timezone: string;
@@ -27,6 +30,7 @@ export class Database extends BaseEntity {
description: string,
details: string,
engine: string,
type: string,
timezone: string,
): Database {
const obj = new Database();
@@ -34,12 +38,20 @@ export class Database extends BaseEntity {
obj.description = description;
obj.connectionConfig = details;
obj.engine = engine;
obj.type = type;
obj.timezone = timezone;
return obj;
}
static toDto(dto: CreateDatabaseDto): Database {
return Database.of(dto.name, dto.description, dto.connectionConfig, dto.engine, dto.timezone);
return Database.of(
dto.name,
dto.description,
dto.connectionConfig,
dto.engine,
dto.type,
dto.timezone,
);
}
getFullDescription(): string {

View File

@@ -5,9 +5,10 @@ import { TypeOrmModule } from '@nestjs/typeorm';
import { Dataset } from './entities/dataset.entity';
import { ConnectionService } from '../connection/connection.service';
import { Database } from '../database/entities/database.entity';
import { Widget } from '../widget/entities/widget.entity';
@Module({
imports: [TypeOrmModule.forFeature([Dataset, Database])],
imports: [TypeOrmModule.forFeature([Dataset, Database, Widget])],
controllers: [DatasetController],
providers: [DatasetService, ConnectionService],
})

View File

@@ -6,12 +6,16 @@ import { Repository } from 'typeorm';
import { Dataset } from './entities/dataset.entity';
import { ConnectionService } from '../connection/connection.service';
import { ResponseStatus } from '../common/enum/response-status.enum';
import { Widget } from '../widget/entities/widget.entity';
import { DatasetType } from '../common/enum/dataset-type.enum';
@Injectable()
export class DatasetService {
constructor(
@InjectRepository(Dataset)
private datasetRepository: Repository<Dataset>,
@InjectRepository(Widget)
private widgetRepository: Repository<Widget>,
private readonly connectionService: ConnectionService,
) {}
@@ -51,8 +55,9 @@ export class DatasetService {
let returnObj: any;
const dataObj = await this.datasetRepository.findOne({ where: { id: id } });
if (!dataObj) returnObj = {status: ResponseStatus.ERROR, message: `id ${id}의 값이 존재하지 않습니다.`};
else returnObj = {status:ResponseStatus.SUCCESS, data: dataObj};
if (!dataObj)
returnObj = { status: ResponseStatus.ERROR, message: `id ${id}의 값이 존재하지 않습니다.` };
else returnObj = { status: ResponseStatus.SUCCESS, data: dataObj };
return returnObj;
}
@@ -89,6 +94,10 @@ export class DatasetService {
return 'Not exist dataset';
} else {
await this.datasetRepository.delete(find_dataset.id);
await this.widgetRepository.delete({
datasetType: DatasetType.DATASET,
datasetId: find_dataset.id,
});
}
return `This action removes a #${id} dataset`;
}

View File

@@ -1,54 +0,0 @@
// import {Connection} from "typeorm";
// import {Seeder, Factory} from 'typeorm-seeding';
// import {Component} from "../component/entities/component.entity";
//
//
// export class CreateInitialData implements Seeder {
// public async run(factory: Factory, connection: Connection): Promise<any> {
// await connection
// .createQueryBuilder()
// .insert()
// .into(Component)
// .values([
// {
// type: 'CHART_LINE',
// title: '선형 차트',
// category: 'LINE',
// icon: 'icon/line-chart.png',
// description: 'Line Chart',
// option: `{
// "title": "test",
// "xField": "",
// "series": [
// {
// "field": "",
// "color": "#5470c6",
// "aggregation": "",
// },
// ],
// "legendPosition": "left",
// }`,
// },
// {
// type: 'CHART_AREA',
// title: '영역형 차트',
// category: 'AREA',
// icon: 'icon/area-chart.png',
// description: 'Area Chart',
// option: `{
// "title": "",
// "xField": "",
// "series": [
// {
// "field": "",
// "color": "#5470c6",
// "aggregation": "",
// },
// ],
// "legendPosition": "left",
// }`,
// }
// ]
// )
// }
// }

View File

@@ -225,7 +225,6 @@ export class TemplateService {
const result = templateComponentInfoList.sort(
(a, b) => b.totalRecommendScore - a.totalRecommendScore,
);
return { status: ResponseStatus.SUCCESS, data: result };
}
@@ -289,7 +288,16 @@ export class TemplateService {
// if (templateInfo.layout.length > i && template) templateInfo.layout[i].i = item.id;
});
templateInfo.widgets = widgetList;
const isWidgetList = widgetList.filter(
widgetItem =>
templateInfo.layout.findIndex(templateItem => templateItem.i === widgetItem.id) > -1,
);
const isntWidgetList = widgetList.filter(
widgetItem =>
templateInfo.layout.findIndex(templateItem => templateItem.i === widgetItem.id) === -1,
);
templateInfo.widgets = isWidgetList.concat(isntWidgetList);
return { status: ResponseStatus.SUCCESS, data: templateInfo };
}
@@ -825,16 +833,23 @@ export class TemplateService {
}
}
// template에서 넘친 widget 목록 가져오기
// // template에서 넘친 widget 목록 가져오기
const leastWidgetList = differntWidgetList.filter(item => item.category != 'MAPPED');
// templateInfo.layout에 새로운 layout object 넣어주기(template에서 넘친 widget 목록)
leastWidgetList.forEach((leastWidget, index) => {
const layout = new DashboardLayout();
layout.x = 0;
layout.y =
templateItemList[templateItemList.length - 1].y +
templateItemList[templateItemList.length - 1].h;
layout.w = 5;
if (index % 2 === 0) {
layout.x = 0;
layout.y =
templateItemList[templateItemList.length - 1].y +
templateItemList[templateItemList.length - 1].h;
} else {
layout.x = 6;
layout.y =
templateItemList[templateItemList.length - 2].y +
templateItemList[templateItemList.length - 2].h;
}
layout.w = 6;
layout.h = 5;
layout.i = leastWidget.id;
templateItemList.push(layout);

View File

@@ -0,0 +1,88 @@
import * as math from 'mathjs';
export const WIDGET_AGGREGATION = {
SUM: 'sum',
AVG: 'avg',
MAX: 'max',
MIN: 'min',
};
/**
*
* @param type
* @param data
* @param field
*/
export const getAggregationData = (type, data, field) => {
let result = 0;
let fits = 0;
let dataList = [];
if (data.length > 0 && (type === WIDGET_AGGREGATION.MIN || type === WIDGET_AGGREGATION.MAX)) {
result = Number(data[0][field]);
} else if (data.length > 0 && type === WIDGET_AGGREGATION.SUM) {
dataList = data.map(row => row[field]);
fits = decimalFits(dataList);
}
switch (type) {
case WIDGET_AGGREGATION.SUM:
data.forEach(item => {
// console.log('item ', item[field]);
if (item[field] != undefined && item[field] != null) {
result += Number(item[field]);
}
});
result = Number(result.toFixed(fits));
break;
case WIDGET_AGGREGATION.AVG:
// result = math.mean(dataList);
data.forEach(item => {
// console.log('item ', item[field]);
if (item[field] != undefined && item[field] != null) {
result = math.add(result, math.bignumber(item[field]));
}
});
result = math.divide(result, math.bignumber(data.length));
// // result = Number(result.toFixed(fits));
// // result = Math.round((result / data.length) * 1000000) / 1000000;
// result = result / data.length;
// result = Number(result);
// result = Math.round(result * 1000000) / 1000000;
result = Number(result.toFixed(6));
break;
case WIDGET_AGGREGATION.MAX:
data.forEach(item => {
// console.log('item ', item[field]);
if (item[field] != undefined && item[field] != null) {
result = Math.max(result, item[field]);
}
});
break;
case WIDGET_AGGREGATION.MIN:
data.forEach(item => {
// console.log('item ', item[field]);
if (item[field] != undefined && item[field] != null) {
result = Math.min(result, item[field]);
}
});
break;
default:
}
return result;
};
function decimalFits(arr) {
//소수점 자리수가 가장많은 수 return
var decimalN = 0;
for (var j = 0; j < arr.length; j++) {
var n = arr[j];
if (!Number.isInteger(n)) {
//소수
var d = String(n).split('.')[1].length; //문자열 소수점 다음 개수
if (decimalN < d) decimalN = d;
}
}
return decimalN;
}
var arr = [0.1, 0.12, 0.123];
decimalFits(arr);

View File

@@ -2,12 +2,15 @@ import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { TableQuery } from './entity/table-query.entity';
import { Database } from '../../database/entities/database.entity';
@Injectable()
export class TableQueryService {
constructor(
@InjectRepository(TableQuery)
private tableQueryRepository: Repository<TableQuery>,
@InjectRepository(Database)
private databaseRepository: Repository<Database>,
) {}
/**
@@ -16,12 +19,33 @@ export class TableQueryService {
* @param tableName
*/
async create(databaseId: number, tableName: string) {
const selectQuery = await this.makeSelectAllQuery(databaseId, tableName);
return await this.tableQueryRepository.save({
databaseId,
query: `SELECT * FROM ${tableName}`,
query: selectQuery,
});
}
async makeSelectAllQuery(databaseId: number, tableName: string) {
const databaseOne = await this.databaseRepository.findOne({ where: { id: databaseId } });
let selectQuery;
switch (databaseOne.type) {
case 'bigquery':
const schemaName = JSON.parse(databaseOne.connectionConfig).connection.schema;
selectQuery = `SELECT * FROM ${schemaName}.${tableName}`;
break;
case 'oracle':
selectQuery = `SELECT * FROM "${tableName}"`;
break;
default:
selectQuery = `SELECT * FROM ${tableName}`;
break;
}
return selectQuery;
}
/**
* tableQuery 삭제
* @param id

View File

@@ -6,9 +6,10 @@ import { Widget } from './entities/widget.entity';
import { Component } from '../component/entities/component.entity';
import { TableQueryService } from './tabel-query/table-query.service';
import { TableQuery } from './tabel-query/entity/table-query.entity';
import { Database } from '../database/entities/database.entity';
@Module({
imports: [TypeOrmModule.forFeature([Widget, Component, TableQuery])],
imports: [TypeOrmModule.forFeature([Widget, Component, TableQuery, Database])],
controllers: [WidgetController],
providers: [WidgetService, TableQueryService],
})

View File

@@ -63,6 +63,7 @@ export class WidgetService {
'component.description as componentDescription',
])
.orderBy('widget.updatedAt', 'DESC')
.addOrderBy('widget.title')
.getRawMany();
find_all.forEach(el => {

View File

@@ -1,5 +1,57 @@
describe('MySQL 연동 확인', () => {
it('test', () => {
return expect(1).toBe(1);
import { Test, TestingModule } from '@nestjs/testing';
import { TypeOrmModule } from '@nestjs/typeorm';
import { getTestMysqlModule } from '../util/get-test-mysql.module';
import { Database } from '../../src/database/entities/database.entity';
import { ConfigModule } from '@nestjs/config';
import { Dataset } from '@google-cloud/bigquery';
import { TableQuery } from '../../src/widget/tabel-query/entity/table-query.entity';
import { DatabaseType } from '../../src/database/entities/database_type.entity';
import { ConnectionService } from '../../src/connection/connection.service';
import * as TestConnectionInfo from '../../test-connect-info.json';
import { ResponseStatus } from '../../src/common/enum/response-status.enum';
describe('QTT-001: 외부 API 연동', () => {
let connectService: ConnectionService;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: '.env.dev',
}),
getTestMysqlModule(),
TypeOrmModule.forFeature([Database, Dataset, TableQuery, DatabaseType]),
],
providers: [ConnectionService],
}).compile();
connectService = module.get<ConnectionService>(ConnectionService);
}, 10000);
const dbName = [
['QTT-001-01', 'mysql'],
['QTT-001-02', 'maria'],
['QTT-001-03', 'pg'],
['QTT-001-04', 'oracle'],
['QTT-001-05', 'cockroach'],
['QTT-001-06', 'redshift'],
['QTT-001-07', 'bigquery'],
['QTT-001-08', 'sqlite'],
['QTT-001-09', 'mssql'],
['QTT-001-10', 'snowflake'],
];
it.each(dbName)('%s : %s', async (name, engine) => {
const result = await connectService.testConnection(Object.create(TestConnectionInfo[engine]));
return expect(result['status']).toStrictEqual(ResponseStatus.SUCCESS);
});
// for (let i = 0; i < dbName.length; i++) {
// it(`QTT-001-${String(i + 1).padStart(2, '0')}`, async () => {
// const result = await connectService.testConnection(
// Object.create(TestConnectionInfo[dbName[i]]),
// );
// return expect(result['status']).toStrictEqual(ResponseStatus.SUCCESS);
// });
// }
});

View File

@@ -0,0 +1,84 @@
import { Test, TestingModule } from '@nestjs/testing';
import { TypeOrmModule } from '@nestjs/typeorm';
import { getTestMysqlModule } from '../util/get-test-mysql.module';
import { Database } from '../../src/database/entities/database.entity';
import { ConfigModule } from '@nestjs/config';
import { TableQuery } from '../../src/widget/tabel-query/entity/table-query.entity';
import { WidgetService } from '../../src/widget/widget.service';
import * as widgetTestoption from './widgetTestOption.json';
import { Widget } from '../../src/widget/entities/widget.entity';
import { Component } from '../../src/component/entities/component.entity';
import { TableQueryService } from '../../src/widget/tabel-query/table-query.service';
import { Connection, DataSource, getConnection, getRepository } from 'typeorm';
import { ResponseStatus } from '../../src/common/enum/response-status.enum';
import { DatasetType } from '../../src/common/enum/dataset-type.enum';
describe('QTT-002 : 위젯 생성', () => {
let widgetService: WidgetService;
let tableQueryService: TableQueryService;
let widgetRepository: Widget;
let connection: Connection;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: `.env.dev`,
}),
getTestMysqlModule(),
TypeOrmModule.forFeature([Widget, Component, TableQuery, Database]),
],
providers: [WidgetService, TableQueryService, Widget],
}).compile();
widgetService = module.get<WidgetService>(WidgetService);
tableQueryService = module.get<TableQueryService>(TableQueryService);
widgetRepository = module.get<Widget>(Widget);
connection = module.get<Connection>(Connection);
}, 10000);
it('QTT-002-01: widget 생성 확인', async () => {
// const result = await widgetService.create(Object.create(widgetTestoption['default']))
const testData = widgetTestoption['default'];
const tableQueryList = [];
const tableData = testData.filter(item => item.datasetType === DatasetType.TABLE);
for (const item of tableData) {
const selectQuery = await tableQueryService.makeSelectAllQuery(
Number(item.databaseId),
item.tableName,
);
tableQueryList.push({ id: item.datasetId, databaseId: item.databaseId, query: selectQuery });
}
await connection.query('truncate table table_query');
const tableQueryResult = await connection
.createQueryBuilder()
.insert()
.into(TableQuery)
.values(tableQueryList)
.execute();
testData.forEach(el => {
el.option = JSON.stringify(el.option);
});
const widgetResult = await connection
.createQueryBuilder()
.insert()
.into(Widget)
.values(testData)
.execute();
return expect(widgetResult.raw.affectedRows).toEqual(100);
});
it('QTT-002-02: widget 목록 확인', async () => {
const result = await widgetService.findAll();
return expect(result.status).toEqual(ResponseStatus.SUCCESS);
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,101 @@
import { Test, TestingModule } from '@nestjs/testing';
import { TypeOrmModule } from '@nestjs/typeorm';
import { getTestMysqlModule } from '../util/get-test-mysql.module';
import { ConfigModule } from '@nestjs/config';
import { Widget } from '../../src/widget/entities/widget.entity';
import { Component } from '../../src/component/entities/component.entity';
import { DashboardService } from '../../src/dashboard/dashboard.service';
import { Dashboard } from '../../src/dashboard/entities/dashboard.entity';
import { DashboardWidget } from '../../src/dashboard/dashboard-widget/entities/dashboard-widget.entity';
import { DashboardWidgetService } from '../../src/dashboard/dashboard-widget/dashboard-widget.service';
import { ComponentService } from '../../src/component/component.service';
import { WidgetService } from '../../src/widget/widget.service';
import { TableQueryService } from '../../src/widget/tabel-query/table-query.service';
import { TableQuery } from '../../src/widget/tabel-query/entity/table-query.entity';
import { Database } from '../../src/database/entities/database.entity';
import { TemplateService } from '../../src/template/template.service';
import { CreateDashboardDto } from '../../src/dashboard/dto/create-dashboard.dto';
import { Template } from '../../src/template/entities/template.entity';
import { TemplateItem } from '../../src/template/entities/template-item.entity';
import { ResponseStatus } from '../../src/common/enum/response-status.enum';
describe('QTT-003: 시각화 종류', () => {
let dashboardService: DashboardService;
let widgetService: WidgetService;
let componentService: ComponentService;
let templateService: TemplateService;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
ConfigModule.forRoot({
isGlobal: true,
envFilePath: '.env.dev',
}),
getTestMysqlModule(),
TypeOrmModule.forFeature([
Dashboard,
DashboardWidget,
Widget,
Component,
TableQuery,
Database,
Template,
TemplateItem,
]),
],
providers: [
DashboardService,
DashboardWidgetService,
WidgetService,
TableQueryService,
ComponentService,
TemplateService,
],
}).compile();
dashboardService = module.get<DashboardService>(DashboardService);
widgetService = module.get<WidgetService>(WidgetService);
componentService = module.get<ComponentService>(ComponentService);
templateService = module.get<TemplateService>(TemplateService);
}, 10000);
const barChartList = [3, 4, 16, 17, 20, 25];
const lineChartList = [1, 2, 14, 15, 26];
const pieChartList = [6, 7, 9, 11, 22, 31, 33, 41, 42, 52, 53];
const etcChartList = [
5, 8, 10, 19, 21, 23, 24, 27, 28, 32, 34, 35, 36, 37, 38, 39, 40, 43, 44, 45, 46, 47, 48, 49,
50, 51,
];
const etcComponentList = [12, 13];
const testData = [
['01: varchart 위젯 목록 확인', 'QTT-003-01 바차트 계열 시각화', barChartList],
['02: linechart 위젯 목록 확인', 'QTT-003-02 라인차트 계열 시각화', lineChartList],
['03: piechart 위젯 목록 확인', 'QTT-003-03 파이차트 계열 시각화', pieChartList],
['04: etcchart 위젯 목록 확인', 'QTT-003-04 기타 차트 계열 시각화 확인', etcChartList],
['05: etcComponent 위젯 목록 확인', 'QTT-003-05 기타 시각화 확인', etcComponentList],
];
it.each(testData)(
'QTT-003-%s',
async (name: string, dashboardTitle: string, componentList: number[]) => {
let findWidgetInfo = await widgetService.findAll();
let widgetIdList = [];
for (let i = 0; i < componentList.length; i++) {
const tempWidgetObj = findWidgetInfo.data.find(
item => item.componentId === componentList[i],
);
widgetIdList.push(tempWidgetObj.id);
}
const layoutResult = await templateService.getTemplateDashboardLayout(widgetIdList, 8);
const createDashboardDto: CreateDashboardDto = new CreateDashboardDto();
createDashboardDto.title = dashboardTitle;
createDashboardDto.layout = layoutResult.data.layout;
const createDashboardResult = await dashboardService.create(createDashboardDto);
console.log('::::::::생성된 대시보드 id :: ', createDashboardResult.data.id);
return expect(createDashboardResult.status).toEqual(ResponseStatus.SUCCESS);
},
);
});

View File

@@ -0,0 +1,33 @@
import * as dummyJson from './QTT-004_dummyData.json';
import * as resultJson from './QTT-004_resultData.json';
import { getAggregationData, WIDGET_AGGREGATION } from '../../src/utils/aggregation.util';
describe('QTT-004: 데이터 집산', () => {
const dummyData = dummyJson.dummyData;
const expectData = resultJson.expectData;
const fieldList = Object.keys(expectData);
it.each(fieldList)('QTT-004-01 (MAX): %s', fieldName => {
expect(getAggregationData(WIDGET_AGGREGATION.MAX, dummyData, fieldName)).toBe(
expectData[fieldName][WIDGET_AGGREGATION.MAX],
);
});
it.each(fieldList)('QTT-004-02 (MIN) : %s ', fieldName => {
expect(getAggregationData(WIDGET_AGGREGATION.MIN, dummyData, fieldName)).toBe(
expectData[fieldName][WIDGET_AGGREGATION.MIN],
);
});
it.each(fieldList)('QTT-004-03 (SUM) : %s ', fieldName => {
expect(getAggregationData(WIDGET_AGGREGATION.SUM, dummyData, fieldName)).toBe(
expectData[fieldName][WIDGET_AGGREGATION.SUM],
);
});
it.each(fieldList)('QTT-004-04 (AVG) : %s ', fieldName => {
expect(getAggregationData(WIDGET_AGGREGATION.AVG, dummyData, fieldName)).toBe(
expectData[fieldName][WIDGET_AGGREGATION.AVG],
);
});
});

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,101 @@
{"expectData":{
"column0" : {"max":998.948,"min":1.116,"sum":407490.778,"avg":509.363473},
"column1" : {"max":999.563,"min":0.385,"sum":409306.359,"avg":511.632949},
"column2" : {"max":999.129,"min":1.168,"sum":389591.141,"avg":486.988926},
"column3" : {"max":999.579,"min":0.424,"sum":419521.521,"avg":524.401901},
"column4" : {"max":998.982,"min":2.311,"sum":393989.918,"avg":492.487398},
"column5" : {"max":999.168,"min":0.23,"sum":391632.037,"avg":489.540046},
"column6" : {"max":998.186,"min":1.201,"sum":393227.412,"avg":491.534265},
"column7" : {"max":996.089,"min":0.031,"sum":413118.872,"avg":516.39859},
"column8" : {"max":998.908,"min":0.04,"sum":400969.224,"avg":501.21153},
"column9" : {"max":998.946,"min":0.191,"sum":414858.579,"avg":518.573224},
"column10" : {"max":996.196,"min":0.987,"sum":404863.735,"avg":506.079669},
"column11" : {"max":997.99,"min":0.345,"sum":401228.814,"avg":501.536018},
"column12" : {"max":996.723,"min":4.326,"sum":399080.727,"avg":498.850909},
"column13" : {"max":998.933,"min":0.558,"sum":380537.694,"avg":475.672118},
"column14" : {"max":998.27,"min":0.087,"sum":395527.684,"avg":494.409605},
"column15" : {"max":997.903,"min":0.894,"sum":405428.704,"avg":506.78588},
"column16" : {"max":998.262,"min":1.628,"sum":399424.256,"avg":499.28032},
"column17" : {"max":999.92,"min":0.14,"sum":405688.163,"avg":507.110204},
"column18" : {"max":998.518,"min":0.968,"sum":409870.981,"avg":512.338726},
"column19" : {"max":998.286,"min":0.618,"sum":394402.714,"avg":493.003393},
"column20" : {"max":997.566,"min":0.01,"sum":407179.516,"avg":508.974395},
"column21" : {"max":995.865,"min":0.536,"sum":410940.916,"avg":513.676145},
"column22" : {"max":999.821,"min":2.186,"sum":415033.819,"avg":518.792274},
"column23" : {"max":999.181,"min":0.272,"sum":409699.541,"avg":512.124426},
"column24" : {"max":999.891,"min":3.872,"sum":406412.265,"avg":508.015331},
"column25" : {"max":999.7,"min":0.568,"sum":392242.444,"avg":490.303055},
"column26" : {"max":999.766,"min":3.807,"sum":400251.747,"avg":500.314684},
"column27" : {"max":999.095,"min":0.21,"sum":398043.371,"avg":497.554214},
"column28" : {"max":995.157,"min":1.177,"sum":404005.741,"avg":505.007176},
"column29" : {"max":999.487,"min":1.012,"sum":394403.681,"avg":493.004601},
"column30" : {"max":998.318,"min":1.629,"sum":409910.569,"avg":512.388211},
"column31" : {"max":998.941,"min":2.153,"sum":396167.899,"avg":495.209874},
"column32" : {"max":998.474,"min":0.083,"sum":401932.824,"avg":502.41603},
"column33" : {"max":999.36,"min":0.308,"sum":385863.073,"avg":482.328841},
"column34" : {"max":999.965,"min":0.383,"sum":399426.485,"avg":499.283106},
"column35" : {"max":999.941,"min":0.828,"sum":406550.077,"avg":508.187596},
"column36" : {"max":998.56,"min":0.534,"sum":393857.778,"avg":492.322223},
"column37" : {"max":998.115,"min":0.827,"sum":397831.11,"avg":497.288888},
"column38" : {"max":997.716,"min":0.154,"sum":405915.964,"avg":507.394955},
"column39" : {"max":998.42,"min":0.062,"sum":409514.766,"avg":511.893458},
"column40" : {"max":999.734,"min":4.852,"sum":398318.867,"avg":497.898584},
"column41" : {"max":999.534,"min":2.031,"sum":407142.789,"avg":508.928486},
"column42" : {"max":999.872,"min":0.701,"sum":412393.392,"avg":515.49174},
"column43" : {"max":999.79,"min":1.502,"sum":403758.409,"avg":504.698011},
"column44" : {"max":998.014,"min":0.54,"sum":395707.726,"avg":494.634658},
"column45" : {"max":996.389,"min":0.287,"sum":389924.446,"avg":487.405558},
"column46" : {"max":999.777,"min":0.555,"sum":408607.805,"avg":510.759756},
"column47" : {"max":999.675,"min":1.107,"sum":402007.437,"avg":502.509296},
"column48" : {"max":997.018,"min":0.993,"sum":406525.938,"avg":508.157423},
"column49" : {"max":999.724,"min":0.109,"sum":391503.017,"avg":489.378771},
"column50" : {"max":999.828,"min":0.321,"sum":408112.412,"avg":510.140515},
"column51" : {"max":999.838,"min":0.21,"sum":393734.577,"avg":492.168221},
"column52" : {"max":999.687,"min":0.401,"sum":393900.081,"avg":492.375101},
"column53" : {"max":999.364,"min":2.952,"sum":396889.218,"avg":496.111523},
"column54" : {"max":999.338,"min":0.216,"sum":384038.187,"avg":480.047734},
"column55" : {"max":998.388,"min":0.765,"sum":405974.245,"avg":507.467806},
"column56" : {"max":999.166,"min":1.002,"sum":404003.733,"avg":505.004666},
"column57" : {"max":999.392,"min":0.194,"sum":399748.594,"avg":499.685743},
"column58" : {"max":998.034,"min":1,"sum":408202.382,"avg":510.252978},
"column59" : {"max":995.775,"min":3.286,"sum":405000.362,"avg":506.250453},
"column60" : {"max":999.323,"min":0.457,"sum":408638.668,"avg":510.798335},
"column61" : {"max":997.392,"min":0.66,"sum":399419.991,"avg":499.274989},
"column62" : {"max":999.664,"min":1.244,"sum":396122.368,"avg":495.15296},
"column63" : {"max":999.831,"min":0.679,"sum":402507.184,"avg":503.13398},
"column64" : {"max":999.674,"min":2.016,"sum":384550.265,"avg":480.687831},
"column65" : {"max":999.83,"min":0.413,"sum":397623.809,"avg":497.029761},
"column66" : {"max":997.354,"min":5.288,"sum":407969.697,"avg":509.962121},
"column67" : {"max":999.658,"min":4.658,"sum":388819.876,"avg":486.024845},
"column68" : {"max":997.769,"min":0.287,"sum":407090.625,"avg":508.863281},
"column69" : {"max":992.847,"min":1.278,"sum":387942.239,"avg":484.927799},
"column70" : {"max":999.816,"min":2.226,"sum":399643.955,"avg":499.554944},
"column71" : {"max":998.834,"min":0.253,"sum":392842.066,"avg":491.052583},
"column72" : {"max":999.686,"min":0.929,"sum":403421.019,"avg":504.276274},
"column73" : {"max":998.759,"min":0.579,"sum":391177.73,"avg":488.972163},
"column74" : {"max":999.886,"min":1.924,"sum":414671.857,"avg":518.339821},
"column75" : {"max":999.963,"min":0.338,"sum":389449.138,"avg":486.811423},
"column76" : {"max":999.567,"min":0.052,"sum":385304.626,"avg":481.630783},
"column77" : {"max":998.847,"min":0.277,"sum":394790.478,"avg":493.488098},
"column78" : {"max":999.334,"min":0.498,"sum":401637.896,"avg":502.04737},
"column79" : {"max":999.635,"min":0.293,"sum":419438.882,"avg":524.298603},
"column80" : {"max":998.16,"min":0.219,"sum":394466.47,"avg":493.083088},
"column81" : {"max":999.658,"min":1.232,"sum":410821.612,"avg":513.527015},
"column82" : {"max":999.718,"min":1.963,"sum":415056.321,"avg":518.820401},
"column83" : {"max":996.498,"min":0.481,"sum":394685.495,"avg":493.356869},
"column84" : {"max":997.869,"min":0.535,"sum":397989.088,"avg":497.48636},
"column85" : {"max":998.859,"min":1.578,"sum":410967.12,"avg":513.7089},
"column86" : {"max":994.368,"min":0.811,"sum":401226.531,"avg":501.533164},
"column87" : {"max":993.338,"min":1.62,"sum":393735.842,"avg":492.169803},
"column88" : {"max":997.111,"min":2.852,"sum":405012.091,"avg":506.265114},
"column89" : {"max":998.836,"min":0.167,"sum":406977.765,"avg":508.722206},
"column90" : {"max":998.146,"min":2.363,"sum":409079.234,"avg":511.349043},
"column91" : {"max":999.407,"min":8.614,"sum":405605.575,"avg":507.006969},
"column92" : {"max":999.129,"min":0.725,"sum":405691.33,"avg":507.114163},
"column93" : {"max":998.867,"min":2.123,"sum":395407.757,"avg":494.259696},
"column94" : {"max":998.492,"min":0.331,"sum":414532.736,"avg":518.16592},
"column95" : {"max":998.944,"min":1.618,"sum":412989.83,"avg":516.237288},
"column96" : {"max":999.886,"min":0.543,"sum":399757.752,"avg":499.69719},
"column97" : {"max":994.608,"min":1.023,"sum":401151.274,"avg":501.439093},
"column98" : {"max":999.864,"min":2.846,"sum":405335.743,"avg":506.669679},
"column99" : {"max":996.95,"min":0.061,"sum":397142,"avg":496.4275}}}

View File

@@ -0,0 +1,47 @@
import { Test, TestingModule } from '@nestjs/testing';
import { TemplateService } from '../../src/template/template.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Template } from '../../src/template/entities/template.entity';
import { TemplateItem } from '../../src/template/entities/template-item.entity';
import { Widget } from '../../src/widget/entities/widget.entity';
import { Component } from '../../src/component/entities/component.entity';
import { getTestMysqlModule } from '../util/get-test-mysql.module';
import { TemplateModule } from '../../src/template/template.module';
import { ConfigModule } from '@nestjs/config';
describe('QTT-005: 대시보드 템플릿', () => {
let templateService: TemplateService;
let templateList;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
TemplateModule,
ConfigModule.forRoot({
isGlobal: true,
envFilePath: '.env.dev',
}),
getTestMysqlModule(),
TypeOrmModule.forFeature([Template, TemplateItem, Widget, Component]),
],
providers: [TemplateService],
}).compile();
templateService = module.get<TemplateService>(TemplateService);
}, 100000);
it('QTT-005-01 : 템플릿 종류 10개 확인', async () => {
const templateResult = await templateService.findAll();
templateList = templateResult.data;
return expect(templateList.length).toEqual(10);
});
afterEach(async () => {
const templateDetail = await templateService.findOne(templateList[0].id);
console.log(
':::::::::::::::::::::::::::::template detail:::::::::::::::::::::::::::::\n',
templateDetail.data,
);
});
});

View File

@@ -0,0 +1,123 @@
import { Test, TestingModule } from '@nestjs/testing';
import { TemplateService } from '../../src/template/template.service';
import { TypeOrmModule } from '@nestjs/typeorm';
import { Template } from '../../src/template/entities/template.entity';
import { TemplateItem } from '../../src/template/entities/template-item.entity';
import { Widget } from '../../src/widget/entities/widget.entity';
import { Component } from '../../src/component/entities/component.entity';
import { getTestMysqlModule } from '../util/get-test-mysql.module';
import { TemplateModule } from '../../src/template/template.module';
import { ConfigModule } from '@nestjs/config';
import { DashboardService } from '../../src/dashboard/dashboard.service';
import { DashboardModule } from '../../src/dashboard/dashboard.module';
import { CreateDashboardDto } from '../../dist/dashboard/dto/create-dashboard.dto';
import { Dashboard } from '../../src/dashboard/entities/dashboard.entity';
import { DashboardWidget } from '../../src/dashboard/dashboard-widget/entities/dashboard-widget.entity';
import { DashboardWidgetService } from '../../src/dashboard/dashboard-widget/dashboard-widget.service';
import { ResponseStatus } from '../../src/common/enum/response-status.enum';
import { WidgetService } from '../../src/widget/widget.service';
import { TableQuery } from '../../src/widget/tabel-query/entity/table-query.entity';
import { Database } from '../../src/database/entities/database.entity';
import { TableQueryService } from '../../src/widget/tabel-query/table-query.service';
describe('QTT-006 : 대시보드 템플릿 추천', () => {
let templateService: TemplateService;
let dashboardService: DashboardService;
let widgetService: WidgetService;
let templateList01, templateList02;
beforeAll(async () => {
const module: TestingModule = await Test.createTestingModule({
imports: [
TemplateModule,
DashboardModule,
ConfigModule.forRoot({
isGlobal: true,
envFilePath: '.env.dev',
}),
getTestMysqlModule(),
TypeOrmModule.forFeature([
Template,
TemplateItem,
Widget,
Component,
Dashboard,
DashboardWidget,
Database,
TableQuery,
]),
],
providers: [
TemplateService,
DashboardService,
DashboardWidgetService,
WidgetService,
TableQueryService,
],
}).compile();
templateService = module.get<TemplateService>(TemplateService);
dashboardService = module.get<DashboardService>(DashboardService);
widgetService = module.get<WidgetService>(WidgetService);
}, 100000);
it('QTT-006-01 : 서로 다른 타입의 위젯 목록', async () => {
const componentList = [15, 12, 41, 13, 38];
let findWidgetInfo = await widgetService.findAll();
let widgetIdList = [];
for (let i = 0; i < componentList.length; i++) {
const tempWidgetObj = findWidgetInfo.data.find(item => item.componentId === componentList[i]);
widgetIdList.push(tempWidgetObj.id);
}
const templateResult = await templateService.findRecommendTemplates(widgetIdList);
templateList01 = templateResult.data;
return expect(templateList01.length).toEqual(10);
});
it('QTT-006-02 : 바차트 타입의 위젯 목록', async () => {
const componentList = [3, 4, 16, 17, 20, 25];
let findWidgetInfo = await widgetService.findAll();
let widgetIdList = [];
for (let i = 0; i < componentList.length; i++) {
const tempWidgetObj = findWidgetInfo.data.find(item => item.componentId === componentList[i]);
widgetIdList.push(tempWidgetObj.id);
}
const templateResult = await templateService.findRecommendTemplates(widgetIdList);
templateList02 = templateResult.data;
// QTT-006-01의 결과와 다름을 확인
return expect(templateList02).not.toEqual(templateList01);
});
it('QTT-006-03 : 알고리즘 범위를 벗어난 위젯 목록', async () => {
const componentList = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
let findWidgetInfo = await widgetService.findAll();
let widgetIdList = [];
for (let i = 0; i < componentList.length; i++) {
const tempWidgetObj = findWidgetInfo.data.find(item => item.componentId === componentList[i]);
widgetIdList.push(tempWidgetObj.id);
}
const templateResult = await templateService.findRecommendTemplates(widgetIdList);
const templateList = templateResult.data;
const layoutResult = await templateService.getTemplateDashboardLayout(
widgetIdList,
templateList[0].id,
);
const createDashboardDto: CreateDashboardDto = new CreateDashboardDto();
createDashboardDto.title = 'QTT-006-03 dashboard';
createDashboardDto.layout = layoutResult.data.layout;
const createDashboardResult = await dashboardService.create(createDashboardDto);
console.log(
'::::::::::::::대시보드 위젯 배치 확인::::::::::::::\n',
createDashboardResult.data,
);
return expect(createDashboardResult.status).toEqual(ResponseStatus.SUCCESS);
});
});

View File

@@ -0,0 +1,25 @@
import { DynamicModule } from '@nestjs/common';
import { TypeOrmModule } from '@nestjs/typeorm';
import path from 'path';
/**
* 테스트 MySQL 가져오기
*
* @returns {DynamicModule}
*/
export function getTestMysqlModule(): DynamicModule {
const entityUrl = path.join(__dirname, '..', '..', '/src/**/*.entity{.ts,.js}');
return TypeOrmModule.forRoot({
type: 'mysql',
host: process.env.DB_HOST,
port: parseInt(process.env.DB_PORT) || 3306,
username: process.env.DB_USERNAME,
password: process.env.DB_PASSWORD,
database: process.env.DB_NAME,
autoLoadEntities: true,
entities: [entityUrl],
synchronize: false,
logging: process.env.NODE_ENV == 'dev',
retryAttempts: 1,
});
}

BIN
design/feature-01.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 281 KiB

BIN
design/feature-02.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 61 KiB

BIN
design/feature-03.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 304 KiB

BIN
design/feature-04.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 131 KiB

BIN
design/vanillameta-logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 KiB

View File

@@ -1 +1,5 @@
REACT_APP_API_URL='http://localhost:4000'
REACT_APP_MODE=prod
REACT_APP_API_URL='http://localhost:4000'
REACT_APP_ID=vanillameta
REACT_APP_PWD=vanillameta
REACT_APP_TOKEN=TOKEN1VB2GS3SWJYDS

View File

@@ -1 +1,5 @@
REACT_APP_API_URL='https://dev-api.vanillameta.net/v1'
REACT_APP_MODE=dev
REACT_APP_API_URL='https://vanillameta-api.vanillabrain.com'
REACT_APP_ID=vanillameta
REACT_APP_PWD=vanillameta
REACT_APP_TOKEN=TOKEN1VB2GS3SWJYDS

View File

@@ -1 +1,5 @@
REACT_APP_API_URL='http://localhost:4000'
REACT_APP_MODE=local
REACT_APP_API_URL='http://localhost:4000'
REACT_APP_ID=vanillameta
REACT_APP_PWD=vanillameta
REACT_APP_TOKEN=TOKEN1VB2GS3SWJYDS

View File

@@ -2,7 +2,8 @@
"env": {
"browser": true,
"es6": true,
"node": true
"node": true,
"jest": true
},
"parser": "@typescript-eslint/parser",
"plugins": ["@typescript-eslint","prettier"],

View File

@@ -0,0 +1,20 @@
{
"moduleFileExtensions": ["js", "json", "jsx", "ts", "tsx", "json"],
"transform": {
"^.+\\.(js|jsx)?$": "babel-jest",
"^.+\\.(ts|tsx)?$": "ts-jest"
},
"testEnvironment": "jsdom",
"moduleDirectories": [
"node_modules",
"src"
],
"moduleNameMapper": {
"@/(.*)": "<rootDir>/src/$1"
},
"testMatch": [
"<rootDir>/**/*.test.(js|jsx|ts|tsx)",
"<rootDir>/(tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx))"
],
"transformIgnorePatterns": ["<rootDir>/node_modules/"]
}

View File

@@ -2256,6 +2256,21 @@
"jest-mock": "^27.5.1"
}
},
"@jest/expect-utils": {
"version": "29.2.2",
"resolved": "https://registry.npmjs.org/@jest/expect-utils/-/expect-utils-29.2.2.tgz",
"integrity": "sha512-vwnVmrVhTmGgQzyvcpze08br91OL61t9O0lJMDyb6Y/D8EKQ9V7rGUb/p7PDt0GPzK0zFYqXWFo4EO2legXmkg==",
"requires": {
"jest-get-type": "^29.2.0"
},
"dependencies": {
"jest-get-type": {
"version": "29.2.0",
"resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.2.0.tgz",
"integrity": "sha512-uXNJlg8hKFEnDgFsrCjznB+sTxdkuqiCL6zMgA75qEbAJjJYTs9XPrvDctrEig2GDow22T/LvHgO57iJhXB/UA=="
}
}
},
"@jest/fake-timers": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/@jest/fake-timers/-/fake-timers-27.5.1.tgz",
@@ -8466,14 +8481,43 @@
}
},
"@types/jest": {
"version": "28.1.6",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-28.1.6.tgz",
"integrity": "sha512-0RbGAFMfcBJKOmqRazM8L98uokwuwD5F8rHrv/ZMbrZBwVOWZUyPG6VFNscjYr/vjM3Vu4fRrCPbOs42AfemaQ==",
"version": "29.2.1",
"resolved": "https://registry.npmjs.org/@types/jest/-/jest-29.2.1.tgz",
"integrity": "sha512-nKixEdnGDqFOZkMTF74avFNr3yRqB1ZJ6sRZv5/28D5x2oLN14KApv7F9mfDT/vUic0L3tRCsh3XWpWjtJisUQ==",
"requires": {
"jest-matcher-utils": "^28.0.0",
"pretty-format": "^28.0.0"
"expect": "^29.0.0",
"pretty-format": "^29.0.0"
},
"dependencies": {
"@jest/schemas": {
"version": "29.0.0",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.0.0.tgz",
"integrity": "sha512-3Ab5HgYIIAnS0HjqJHQYZS+zXc4tUmTmBH3z83ajI6afXp8X3ZtdLX+nXx+I7LNkJD7uN9LAVhgnjDgZa2z0kA==",
"requires": {
"@sinclair/typebox": "^0.24.1"
}
},
"@jest/types": {
"version": "29.2.1",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-29.2.1.tgz",
"integrity": "sha512-O/QNDQODLnINEPAI0cl9U6zUIDXEWXt6IC1o2N2QENuos7hlGUIthlKyV4p6ki3TvXFX071blj8HUhgLGquPjw==",
"requires": {
"@jest/schemas": "^29.0.0",
"@types/istanbul-lib-coverage": "^2.0.0",
"@types/istanbul-reports": "^3.0.0",
"@types/node": "*",
"@types/yargs": "^17.0.8",
"chalk": "^4.0.0"
}
},
"@types/yargs": {
"version": "17.0.13",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.13.tgz",
"integrity": "sha512-9sWaruZk2JGxIQU+IhI1fhPYRcQ0UuTNuKuCW9bR5fp7qi2Llf7WDzNa17Cy7TKnh3cdxDOiyTu6gaLS0eDatg==",
"requires": {
"@types/yargs-parser": "*"
}
},
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
@@ -8505,9 +8549,21 @@
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"diff-sequences": {
"version": "28.1.1",
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-28.1.1.tgz",
"integrity": "sha512-FU0iFaH/E23a+a718l8Qa/19bF9p06kgE0KipMOMadwa3SjnaElKzPaUC0vnibs6/B/9ni97s61mcejk8W1fQw=="
"version": "29.2.0",
"resolved": "https://registry.npmjs.org/diff-sequences/-/diff-sequences-29.2.0.tgz",
"integrity": "sha512-413SY5JpYeSBZxmenGEmCVQ8mCgtFJF0w9PROdaS6z987XC2Pd2GOKqOITLtMftmyFZqgtCOb/QA7/Z3ZXfzIw=="
},
"expect": {
"version": "29.2.2",
"resolved": "https://registry.npmjs.org/expect/-/expect-29.2.2.tgz",
"integrity": "sha512-hE09QerxZ5wXiOhqkXy5d2G9ar+EqOyifnCXCpMNu+vZ6DG9TJ6CO2c2kPDSLqERTTWrO7OZj8EkYHQqSd78Yw==",
"requires": {
"@jest/expect-utils": "^29.2.2",
"jest-get-type": "^29.2.0",
"jest-matcher-utils": "^29.2.2",
"jest-message-util": "^29.2.1",
"jest-util": "^29.2.1"
}
},
"has-flag": {
"version": "4.0.0",
@@ -8515,39 +8571,67 @@
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
},
"jest-diff": {
"version": "28.1.3",
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-28.1.3.tgz",
"integrity": "sha512-8RqP1B/OXzjjTWkqMX67iqgwBVJRgCyKD3L9nq+6ZqJMdvjE8RgHktqZ6jNrkdMT+dJuYNI3rhQpxaz7drJHfw==",
"version": "29.2.1",
"resolved": "https://registry.npmjs.org/jest-diff/-/jest-diff-29.2.1.tgz",
"integrity": "sha512-gfh/SMNlQmP3MOUgdzxPOd4XETDJifADpT937fN1iUGz+9DgOu2eUPHH25JDkLVcLwwqxv3GzVyK4VBUr9fjfA==",
"requires": {
"chalk": "^4.0.0",
"diff-sequences": "^28.1.1",
"jest-get-type": "^28.0.2",
"pretty-format": "^28.1.3"
"diff-sequences": "^29.2.0",
"jest-get-type": "^29.2.0",
"pretty-format": "^29.2.1"
}
},
"jest-get-type": {
"version": "28.0.2",
"resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-28.0.2.tgz",
"integrity": "sha512-ioj2w9/DxSYHfOm5lJKCdcAmPJzQXmbM/Url3rhlghrPvT3tt+7a/+oXc9azkKmLvoiXjtV83bEWqi+vs5nlPA=="
"version": "29.2.0",
"resolved": "https://registry.npmjs.org/jest-get-type/-/jest-get-type-29.2.0.tgz",
"integrity": "sha512-uXNJlg8hKFEnDgFsrCjznB+sTxdkuqiCL6zMgA75qEbAJjJYTs9XPrvDctrEig2GDow22T/LvHgO57iJhXB/UA=="
},
"jest-matcher-utils": {
"version": "28.1.3",
"resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-28.1.3.tgz",
"integrity": "sha512-kQeJ7qHemKfbzKoGjHHrRKH6atgxMk8Enkk2iPQ3XwO6oE/KYD8lMYOziCkeSB9G4adPM4nR1DE8Tf5JeWH6Bw==",
"version": "29.2.2",
"resolved": "https://registry.npmjs.org/jest-matcher-utils/-/jest-matcher-utils-29.2.2.tgz",
"integrity": "sha512-4DkJ1sDPT+UX2MR7Y3od6KtvRi9Im1ZGLGgdLFLm4lPexbTaCgJW5NN3IOXlQHF7NSHY/VHhflQ+WoKtD/vyCw==",
"requires": {
"chalk": "^4.0.0",
"jest-diff": "^28.1.3",
"jest-get-type": "^28.0.2",
"pretty-format": "^28.1.3"
"jest-diff": "^29.2.1",
"jest-get-type": "^29.2.0",
"pretty-format": "^29.2.1"
}
},
"jest-message-util": {
"version": "29.2.1",
"resolved": "https://registry.npmjs.org/jest-message-util/-/jest-message-util-29.2.1.tgz",
"integrity": "sha512-Dx5nEjw9V8C1/Yj10S/8ivA8F439VS8vTq1L7hEgwHFn9ovSKNpYW/kwNh7UglaEgXO42XxzKJB+2x0nSglFVw==",
"requires": {
"@babel/code-frame": "^7.12.13",
"@jest/types": "^29.2.1",
"@types/stack-utils": "^2.0.0",
"chalk": "^4.0.0",
"graceful-fs": "^4.2.9",
"micromatch": "^4.0.4",
"pretty-format": "^29.2.1",
"slash": "^3.0.0",
"stack-utils": "^2.0.3"
}
},
"jest-util": {
"version": "29.2.1",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.2.1.tgz",
"integrity": "sha512-P5VWDj25r7kj7kl4pN2rG/RN2c1TLfYYYZYULnS/35nFDjBai+hBeo3MDrYZS7p6IoY3YHZnt2vq4L6mKnLk0g==",
"requires": {
"@jest/types": "^29.2.1",
"@types/node": "*",
"chalk": "^4.0.0",
"ci-info": "^3.2.0",
"graceful-fs": "^4.2.9",
"picomatch": "^2.2.3"
}
},
"pretty-format": {
"version": "28.1.3",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-28.1.3.tgz",
"integrity": "sha512-8gFb/To0OmxHR9+ZTb14Df2vNxdGCX8g1xWGUTqUw5TiZvcQf5sHKObd5UcPyLLyowNwDAMTF3XWOG1B6mxl1Q==",
"version": "29.2.1",
"resolved": "https://registry.npmjs.org/pretty-format/-/pretty-format-29.2.1.tgz",
"integrity": "sha512-Y41Sa4aLCtKAXvwuIpTvcFBkyeYp2gdFWzXGA+ZNES3VwURIB165XO/z7CjETwzCCS53MjW/rLMyyqEnTtaOfA==",
"requires": {
"@jest/schemas": "^28.1.3",
"ansi-regex": "^5.0.1",
"@jest/schemas": "^29.0.0",
"ansi-styles": "^5.0.0",
"react-is": "^18.0.0"
},
@@ -9863,24 +9947,90 @@
"integrity": "sha512-Td525n+iPOOyUQIeBfcASuG6uJsDOITl7Mds5gFyerkWiX7qhUTdYUBlSgNMyVqtSJqwpt1kXGLdUt6SykLMRA=="
},
"babel-jest": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-27.5.1.tgz",
"integrity": "sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg==",
"version": "29.2.2",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-29.2.2.tgz",
"integrity": "sha512-kkq2QSDIuvpgfoac3WZ1OOcHsQQDU5xYk2Ql7tLdJ8BVAYbefEXal+NfS45Y5LVZA7cxC8KYcQMObpCt1J025w==",
"dev": true,
"requires": {
"@jest/transform": "^27.5.1",
"@jest/types": "^27.5.1",
"@jest/transform": "^29.2.2",
"@types/babel__core": "^7.1.14",
"babel-plugin-istanbul": "^6.1.1",
"babel-preset-jest": "^27.5.1",
"babel-preset-jest": "^29.2.0",
"chalk": "^4.0.0",
"graceful-fs": "^4.2.9",
"slash": "^3.0.0"
},
"dependencies": {
"@jest/schemas": {
"version": "29.0.0",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.0.0.tgz",
"integrity": "sha512-3Ab5HgYIIAnS0HjqJHQYZS+zXc4tUmTmBH3z83ajI6afXp8X3ZtdLX+nXx+I7LNkJD7uN9LAVhgnjDgZa2z0kA==",
"dev": true,
"requires": {
"@sinclair/typebox": "^0.24.1"
}
},
"@jest/transform": {
"version": "29.2.2",
"resolved": "https://registry.npmjs.org/@jest/transform/-/transform-29.2.2.tgz",
"integrity": "sha512-aPe6rrletyuEIt2axxgdtxljmzH8O/nrov4byy6pDw9S8inIrTV+2PnjyP/oFHMSynzGxJ2s6OHowBNMXp/Jzg==",
"dev": true,
"requires": {
"@babel/core": "^7.11.6",
"@jest/types": "^29.2.1",
"@jridgewell/trace-mapping": "^0.3.15",
"babel-plugin-istanbul": "^6.1.1",
"chalk": "^4.0.0",
"convert-source-map": "^1.4.0",
"fast-json-stable-stringify": "^2.1.0",
"graceful-fs": "^4.2.9",
"jest-haste-map": "^29.2.1",
"jest-regex-util": "^29.2.0",
"jest-util": "^29.2.1",
"micromatch": "^4.0.4",
"pirates": "^4.0.4",
"slash": "^3.0.0",
"write-file-atomic": "^4.0.1"
}
},
"@jest/types": {
"version": "29.2.1",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-29.2.1.tgz",
"integrity": "sha512-O/QNDQODLnINEPAI0cl9U6zUIDXEWXt6IC1o2N2QENuos7hlGUIthlKyV4p6ki3TvXFX071blj8HUhgLGquPjw==",
"dev": true,
"requires": {
"@jest/schemas": "^29.0.0",
"@types/istanbul-lib-coverage": "^2.0.0",
"@types/istanbul-reports": "^3.0.0",
"@types/node": "*",
"@types/yargs": "^17.0.8",
"chalk": "^4.0.0"
}
},
"@jridgewell/trace-mapping": {
"version": "0.3.17",
"resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.17.tgz",
"integrity": "sha512-MCNzAp77qzKca9+W/+I0+sEpaUnZoeasnghNeVc41VZCEKaCH73Vq3BZZ/SzWIgrqE4H4ceI+p+b6C0mHf9T4g==",
"dev": true,
"requires": {
"@jridgewell/resolve-uri": "3.1.0",
"@jridgewell/sourcemap-codec": "1.4.14"
}
},
"@types/yargs": {
"version": "17.0.13",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.13.tgz",
"integrity": "sha512-9sWaruZk2JGxIQU+IhI1fhPYRcQ0UuTNuKuCW9bR5fp7qi2Llf7WDzNa17Cy7TKnh3cdxDOiyTu6gaLS0eDatg==",
"dev": true,
"requires": {
"@types/yargs-parser": "*"
}
},
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"requires": {
"color-convert": "^2.0.1"
}
@@ -9889,6 +10039,7 @@
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
@@ -9898,6 +10049,7 @@
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"requires": {
"color-name": "~1.1.4"
}
@@ -9905,20 +10057,96 @@
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true
},
"jest-haste-map": {
"version": "29.2.1",
"resolved": "https://registry.npmjs.org/jest-haste-map/-/jest-haste-map-29.2.1.tgz",
"integrity": "sha512-wF460rAFmYc6ARcCFNw4MbGYQjYkvjovb9GBT+W10Um8q5nHq98jD6fHZMDMO3tA56S8XnmNkM8GcA8diSZfnA==",
"dev": true,
"requires": {
"@jest/types": "^29.2.1",
"@types/graceful-fs": "^4.1.3",
"@types/node": "*",
"anymatch": "^3.0.3",
"fb-watchman": "^2.0.0",
"fsevents": "^2.3.2",
"graceful-fs": "^4.2.9",
"jest-regex-util": "^29.2.0",
"jest-util": "^29.2.1",
"jest-worker": "^29.2.1",
"micromatch": "^4.0.4",
"walker": "^1.0.8"
}
},
"jest-regex-util": {
"version": "29.2.0",
"resolved": "https://registry.npmjs.org/jest-regex-util/-/jest-regex-util-29.2.0.tgz",
"integrity": "sha512-6yXn0kg2JXzH30cr2NlThF+70iuO/3irbaB4mh5WyqNIvLLP+B6sFdluO1/1RJmslyh/f9osnefECflHvTbwVA==",
"dev": true
},
"jest-util": {
"version": "29.2.1",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.2.1.tgz",
"integrity": "sha512-P5VWDj25r7kj7kl4pN2rG/RN2c1TLfYYYZYULnS/35nFDjBai+hBeo3MDrYZS7p6IoY3YHZnt2vq4L6mKnLk0g==",
"dev": true,
"requires": {
"@jest/types": "^29.2.1",
"@types/node": "*",
"chalk": "^4.0.0",
"ci-info": "^3.2.0",
"graceful-fs": "^4.2.9",
"picomatch": "^2.2.3"
}
},
"jest-worker": {
"version": "29.2.1",
"resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.2.1.tgz",
"integrity": "sha512-ROHTZ+oj7sBrgtv46zZ84uWky71AoYi0vEV9CdEtc1FQunsoAGe5HbQmW76nI5QWdvECVPrSi1MCVUmizSavMg==",
"dev": true,
"requires": {
"@types/node": "*",
"jest-util": "^29.2.1",
"merge-stream": "^2.0.0",
"supports-color": "^8.0.0"
},
"dependencies": {
"supports-color": {
"version": "8.1.1",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz",
"integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==",
"dev": true,
"requires": {
"has-flag": "^4.0.0"
}
}
}
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"requires": {
"has-flag": "^4.0.0"
}
},
"write-file-atomic": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-4.0.2.tgz",
"integrity": "sha512-7KxauUdBmSdWnmpaGFg+ppNjKF8uNLry8LyzjauQDOVONfFLNKrKvQOxZ/VuTIcS/gge/YNahf5RIIQWTSarlg==",
"dev": true,
"requires": {
"imurmurhash": "^0.1.4",
"signal-exit": "^3.0.7"
}
}
}
},
@@ -10007,13 +10235,14 @@
}
},
"babel-plugin-jest-hoist": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz",
"integrity": "sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ==",
"version": "29.2.0",
"resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-29.2.0.tgz",
"integrity": "sha512-TnspP2WNiR3GLfCsUNHqeXw0RoQ2f9U5hQ5L3XFpwuO8htQmSrhh8qsB6vi5Yi8+kuynN1yjDjQsPfkebmB6ZA==",
"dev": true,
"requires": {
"@babel/template": "^7.3.3",
"@babel/types": "^7.3.3",
"@types/babel__core": "^7.0.0",
"@types/babel__core": "^7.1.14",
"@types/babel__traverse": "^7.0.6"
}
},
@@ -10125,11 +10354,12 @@
}
},
"babel-preset-jest": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz",
"integrity": "sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag==",
"version": "29.2.0",
"resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-29.2.0.tgz",
"integrity": "sha512-z9JmMJppMxNv8N7fNRHvhMg9cvIkMxQBXgFkane3yKVEvEOP+kB50lk8DFRvF9PGqbyXxlmebKWhuDORO8RgdA==",
"dev": true,
"requires": {
"babel-plugin-jest-hoist": "^27.5.1",
"babel-plugin-jest-hoist": "^29.2.0",
"babel-preset-current-node-syntax": "^1.0.0"
}
},
@@ -10587,6 +10817,15 @@
"update-browserslist-db": "^1.0.4"
}
},
"bs-logger": {
"version": "0.2.6",
"resolved": "https://registry.npmjs.org/bs-logger/-/bs-logger-0.2.6.tgz",
"integrity": "sha512-pd8DCoxmbgc7hyPKOvxtqNcjYoOsABPQdcCUjGp3d42VR2CX1ORhk2A87oqqu5R1kk+76nsxZupkmyd+MVtCog==",
"dev": true,
"requires": {
"fast-json-stable-stringify": "2.x"
}
},
"bser": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/bser/-/bser-2.1.1.tgz",
@@ -11228,6 +11467,11 @@
"resolved": "https://registry.npmjs.org/commondir/-/commondir-1.0.1.tgz",
"integrity": "sha512-W9pAhw0ja1Edb5GVdIF1mjZw/ASI0AlShXM83UUGe2DVr5TdAPEA1OA8m/g8zWp9x6On7gqufY+FatDbC3MDQg=="
},
"complex.js": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/complex.js/-/complex.js-2.1.1.tgz",
"integrity": "sha512-8njCHOTtFFLtegk6zQo0kkVX1rngygb/KQI6z1qZxlFI3scluC+LVTCFbrkWjBv4vvLlbQ9t88IPMC6k95VTTg=="
},
"component-emitter": {
"version": "1.3.0",
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
@@ -12897,6 +13141,11 @@
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
"integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow=="
},
"escape-latex": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/escape-latex/-/escape-latex-1.2.0.tgz",
"integrity": "sha512-nV5aVWW1K0wEiUIEdZ4erkGGH8mDxGyxSeqPzRNtWP7ataw+/olFObw7hujFWlVjNsaDFw5VZ5NzVSIqRgfTiw=="
},
"escape-string-regexp": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
@@ -15987,6 +16236,11 @@
}
}
},
"javascript-natural-sort": {
"version": "0.7.1",
"resolved": "https://registry.npmjs.org/javascript-natural-sort/-/javascript-natural-sort-0.7.1.tgz",
"integrity": "sha512-nO6jcEfZWQXDhOiBtG2KvKyEptz7RVbpGP4vTD2hLBdmNQSsCiicO2Ioinv6UI4y9ukqnBpy+XZ9H6uLNgJTlw=="
},
"jest": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/jest/-/jest-27.5.1.tgz",
@@ -16181,6 +16435,41 @@
"color-convert": "^2.0.1"
}
},
"babel-jest": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-27.5.1.tgz",
"integrity": "sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg==",
"requires": {
"@jest/transform": "^27.5.1",
"@jest/types": "^27.5.1",
"@types/babel__core": "^7.1.14",
"babel-plugin-istanbul": "^6.1.1",
"babel-preset-jest": "^27.5.1",
"chalk": "^4.0.0",
"graceful-fs": "^4.2.9",
"slash": "^3.0.0"
}
},
"babel-plugin-jest-hoist": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz",
"integrity": "sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ==",
"requires": {
"@babel/template": "^7.3.3",
"@babel/types": "^7.3.3",
"@types/babel__core": "^7.0.0",
"@types/babel__traverse": "^7.0.6"
}
},
"babel-preset-jest": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz",
"integrity": "sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag==",
"requires": {
"babel-plugin-jest-hoist": "^27.5.1",
"babel-preset-current-node-syntax": "^1.0.0"
}
},
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -17810,6 +18099,37 @@
"resolved": "https://registry.npmjs.org/material-colors/-/material-colors-1.2.6.tgz",
"integrity": "sha512-6qE4B9deFBIa9YSpOc9O0Sgc43zTeVYbgDT5veRKSlB2+ZuHNoVVxA1L/ckMUayV9Ay9y7Z/SZCLcGteW9i7bg=="
},
"mathjs": {
"version": "11.3.3",
"resolved": "https://registry.npmjs.org/mathjs/-/mathjs-11.3.3.tgz",
"integrity": "sha512-+NsgPRzvnczrw5hp7fNPMnfXCaBo2cs7c8Edoacbjcc2Z3js6jHf+Pz8FY6nb5udmBj0q4zl+a5PxbjWVgOQcA==",
"requires": {
"@babel/runtime": "^7.20.1",
"complex.js": "^2.1.1",
"decimal.js": "^10.4.2",
"escape-latex": "^1.2.0",
"fraction.js": "^4.2.0",
"javascript-natural-sort": "^0.7.1",
"seedrandom": "^3.0.5",
"tiny-emitter": "^2.1.0",
"typed-function": "^4.1.0"
},
"dependencies": {
"@babel/runtime": {
"version": "7.20.1",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.20.1.tgz",
"integrity": "sha512-mrzLkl6U9YLF8qpqI7TB82PESyEGjm/0Ly91jG575eVxMMlb8fYfOXFZIJ8XfLrJZQbm7dlKry2bJmXBUEkdFg==",
"requires": {
"regenerator-runtime": "^0.13.10"
}
},
"regenerator-runtime": {
"version": "0.13.10",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.10.tgz",
"integrity": "sha512-KepLsg4dU12hryUO7bp/axHAKvwGOCV0sGloQtpagJ12ai+ojVDqkeGSiRX1zlq+kjIMZ1t7gpze+26QqtdGqw=="
}
}
},
"md5.js": {
"version": "1.3.5",
"resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz",
@@ -20734,6 +21054,41 @@
"resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz",
"integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q=="
},
"babel-jest": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/babel-jest/-/babel-jest-27.5.1.tgz",
"integrity": "sha512-cdQ5dXjGRd0IBRATiQ4mZGlGlRE8kJpjPOixdNRdT+m3UcNqmYWN6rK6nvtXYfY3D76cb8s/O1Ss8ea24PIwcg==",
"requires": {
"@jest/transform": "^27.5.1",
"@jest/types": "^27.5.1",
"@types/babel__core": "^7.1.14",
"babel-plugin-istanbul": "^6.1.1",
"babel-preset-jest": "^27.5.1",
"chalk": "^4.0.0",
"graceful-fs": "^4.2.9",
"slash": "^3.0.0"
}
},
"babel-plugin-jest-hoist": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/babel-plugin-jest-hoist/-/babel-plugin-jest-hoist-27.5.1.tgz",
"integrity": "sha512-50wCwD5EMNW4aRpOwtqzyZHIewTYNxLA4nhB+09d8BIssfNfzBRhkBIHiaPv1Si226TQSvp8gxAJm2iY2qs2hQ==",
"requires": {
"@babel/template": "^7.3.3",
"@babel/types": "^7.3.3",
"@types/babel__core": "^7.0.0",
"@types/babel__traverse": "^7.0.6"
}
},
"babel-preset-jest": {
"version": "27.5.1",
"resolved": "https://registry.npmjs.org/babel-preset-jest/-/babel-preset-jest-27.5.1.tgz",
"integrity": "sha512-Nptf2FzlPCWYuJg41HBqXVT8ym6bXOevuCTbhxlUpjwtysGaIWFvDEjp4y+G7fl13FgOdjs7P/DmErqH7da0Ag==",
"requires": {
"babel-plugin-jest-hoist": "^27.5.1",
"babel-preset-current-node-syntax": "^1.0.0"
}
},
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
@@ -21842,6 +22197,11 @@
"ajv-keywords": "^3.5.2"
}
},
"seedrandom": {
"version": "3.0.5",
"resolved": "https://registry.npmjs.org/seedrandom/-/seedrandom-3.0.5.tgz",
"integrity": "sha512-8OwmbklUNzwezjGInmZ+2clQmExQPvomqjL7LFqOYqtmuxRgQYqOD3mHaU+MvZn5FLUeVxVfQjwLZW/n/JFuqg=="
},
"select-hose": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz",
@@ -23416,6 +23776,11 @@
"setimmediate": "^1.0.4"
}
},
"tiny-emitter": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/tiny-emitter/-/tiny-emitter-2.1.0.tgz",
"integrity": "sha512-NB6Dk1A9xgQPMoGqC5CVXn123gWyte215ONT5Pp5a0yt4nlEoO1ZWeCwpncaekPHXO60i47ihFnZPiRPjRMq4Q=="
},
"tiny-glob": {
"version": "0.2.9",
"resolved": "https://registry.npmjs.org/tiny-glob/-/tiny-glob-0.2.9.tgz",
@@ -23554,6 +23919,125 @@
"integrity": "sha512-q5W7tVM71e2xjHZTlgfTDoPF/SmqKG5hddq9SzR49CH2hayqRKJtQ4mtRlSxKaJlR/+9rEM+mnBHf7I2/BQcpQ==",
"dev": true
},
"ts-jest": {
"version": "29.0.3",
"resolved": "https://registry.npmjs.org/ts-jest/-/ts-jest-29.0.3.tgz",
"integrity": "sha512-Ibygvmuyq1qp/z3yTh9QTwVVAbFdDy/+4BtIQR2sp6baF2SJU/8CKK/hhnGIDY2L90Az2jIqTwZPnN2p+BweiQ==",
"dev": true,
"requires": {
"bs-logger": "0.x",
"fast-json-stable-stringify": "2.x",
"jest-util": "^29.0.0",
"json5": "^2.2.1",
"lodash.memoize": "4.x",
"make-error": "1.x",
"semver": "7.x",
"yargs-parser": "^21.0.1"
},
"dependencies": {
"@jest/schemas": {
"version": "29.0.0",
"resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.0.0.tgz",
"integrity": "sha512-3Ab5HgYIIAnS0HjqJHQYZS+zXc4tUmTmBH3z83ajI6afXp8X3ZtdLX+nXx+I7LNkJD7uN9LAVhgnjDgZa2z0kA==",
"dev": true,
"requires": {
"@sinclair/typebox": "^0.24.1"
}
},
"@jest/types": {
"version": "29.2.1",
"resolved": "https://registry.npmjs.org/@jest/types/-/types-29.2.1.tgz",
"integrity": "sha512-O/QNDQODLnINEPAI0cl9U6zUIDXEWXt6IC1o2N2QENuos7hlGUIthlKyV4p6ki3TvXFX071blj8HUhgLGquPjw==",
"dev": true,
"requires": {
"@jest/schemas": "^29.0.0",
"@types/istanbul-lib-coverage": "^2.0.0",
"@types/istanbul-reports": "^3.0.0",
"@types/node": "*",
"@types/yargs": "^17.0.8",
"chalk": "^4.0.0"
}
},
"@types/yargs": {
"version": "17.0.13",
"resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.13.tgz",
"integrity": "sha512-9sWaruZk2JGxIQU+IhI1fhPYRcQ0UuTNuKuCW9bR5fp7qi2Llf7WDzNa17Cy7TKnh3cdxDOiyTu6gaLS0eDatg==",
"dev": true,
"requires": {
"@types/yargs-parser": "*"
}
},
"ansi-styles": {
"version": "4.3.0",
"resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz",
"integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==",
"dev": true,
"requires": {
"color-convert": "^2.0.1"
}
},
"chalk": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz",
"integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==",
"dev": true,
"requires": {
"ansi-styles": "^4.1.0",
"supports-color": "^7.1.0"
}
},
"color-convert": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz",
"integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==",
"dev": true,
"requires": {
"color-name": "~1.1.4"
}
},
"color-name": {
"version": "1.1.4",
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
"dev": true
},
"has-flag": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz",
"integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==",
"dev": true
},
"jest-util": {
"version": "29.2.1",
"resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.2.1.tgz",
"integrity": "sha512-P5VWDj25r7kj7kl4pN2rG/RN2c1TLfYYYZYULnS/35nFDjBai+hBeo3MDrYZS7p6IoY3YHZnt2vq4L6mKnLk0g==",
"dev": true,
"requires": {
"@jest/types": "^29.2.1",
"@types/node": "*",
"chalk": "^4.0.0",
"ci-info": "^3.2.0",
"graceful-fs": "^4.2.9",
"picomatch": "^2.2.3"
}
},
"supports-color": {
"version": "7.2.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
"integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==",
"dev": true,
"requires": {
"has-flag": "^4.0.0"
}
},
"yargs-parser": {
"version": "21.1.1",
"resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-21.1.1.tgz",
"integrity": "sha512-tVpsJW7DdjecAiFpbIB1e3qxIQsE6NoPc5/eTdrbbIC4h0LVsWhnoa3g+m2HclBIujHzsxZ4VJVA+GUuc2/LBw==",
"dev": true
}
}
},
"ts-node": {
"version": "10.9.1",
"resolved": "https://registry.npmjs.org/ts-node/-/ts-node-10.9.1.tgz",
@@ -23703,6 +24187,11 @@
"mime-types": "~2.1.24"
}
},
"typed-function": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/typed-function/-/typed-function-4.1.0.tgz",
"integrity": "sha512-DGwUl6cioBW5gw2L+6SMupGwH/kZOqivy17E4nsh1JI9fKF87orMmlQx3KISQPmg3sfnOUGlwVkroosvgddrlg=="
},
"typedarray": {
"version": "0.0.6",
"resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz",

View File

@@ -26,6 +26,7 @@
"echarts": "^5.3.3",
"echarts-for-react": "^3.0.2",
"echarts-gl": "^2.0.9",
"mathjs": "^11.3.3",
"react": "^18.2.0",
"react-ace": "^10.1.0",
"react-alert": "^7.0.3",
@@ -42,12 +43,12 @@
"web-vitals": "^2.1.4"
},
"scripts": {
"start": "craco start",
"start": "env-cmd -f .env.local craco start",
"start:dev": "env-cmd -f .env.development craco start",
"start:local": "env-cmd -f .env.local craco start",
"build": "craco build",
"build:dev": "env-cmd -f .env.development craco build",
"test": "craco test",
"test": "jest",
"eject": "craco eject",
"storybook": "start-storybook -p 6006",
"build-storybook": "build-storybook"
@@ -95,9 +96,11 @@
"@storybook/preset-create-react-app": "^4.1.2",
"@storybook/react": "^6.5.9",
"@storybook/testing-library": "0.0.13",
"@types/jest": "^29.2.1",
"@types/numeral": "^2.0.2",
"@typescript-eslint/eslint-plugin": "^5.30.7",
"@typescript-eslint/parser": "^5.30.7",
"babel-jest": "^29.2.2",
"babel-loader": "^8.2.5",
"babel-plugin-named-exports-order": "0.0.2",
"craco-alias": "^3.0.1",
@@ -116,6 +119,7 @@
"prop-types": "^15.8.1",
"react-error-overlay": "^6.0.11",
"serve": "^14.0.1",
"ts-jest": "^29.0.3",
"typescript": "^4.7.4",
"webpack": "^5.73.0"
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.8 KiB

After

Width:  |  Height:  |  Size: 32 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 55 KiB

File diff suppressed because one or more lines are too long

After

Width:  |  Height:  |  Size: 246 KiB

View File

@@ -1,6 +1,5 @@
import React, { useEffect } from 'react';
import React from 'react';
import { CssBaseline } from '@mui/material';
import Layout from './layouts/Layout';
import Router from './router';
import 'tui-grid/dist/tui-grid.css';
import Grid from 'tui-grid';
@@ -59,9 +58,7 @@ function App() {
return (
<>
<CssBaseline />
<Layout>
<Router />
</Layout>
<Router />
</>
);
}

View File

@@ -3,6 +3,7 @@ import { del, get, post, put } from '@/helpers/apiHelper';
export const URL_DATABASE = '/database';
const selectDatabaseList = (): Promise<any> => get(URL_DATABASE);
const selectDatabaseInfo = (id: string, data = null): Promise<any> => get(URL_DATABASE + '/info/' + id, data);
const selectDatabase = (id: string, data = null): Promise<any> => get(URL_DATABASE + '/' + id, data);
const testConnection = (data: unknown): Promise<any> => post(URL_DATABASE + '/test', data);
const executeQuery = (data: unknown): Promise<any> => post(URL_DATABASE + '/execute', data);
@@ -14,6 +15,7 @@ const selectData = (data: unknown): Promise<any> => get(URL_DATABASE + '/data',
const DatabaseService = {
selectDatabaseList,
selectDatabaseInfo,
selectDatabase,
selectDatabaseTypeList,
createDatabase,

View File

@@ -1,3 +1,3 @@
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 13a.996.996 0 0 1-.707-.293l-5-5a1 1 0 0 1 0-1.414l5-5a1 1 0 1 1 1.414 1.414L5.414 7l4.294 4.294A1 1 0 0 1 9 13z" fill="#fff"/>
<path d="M9 13a.996.996 0 0 1-.707-.293l-5-5a1 1 0 0 1 0-1.414l5-5a1 1 0 1 1 1.414 1.414L5.414 7l4.294 4.294A1 1 0 0 1 9 13z" />
</svg>

Before

Width:  |  Height:  |  Size: 247 B

After

Width:  |  Height:  |  Size: 236 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

View File

@@ -49,7 +49,7 @@ function BoardList(props) {
<Stack
flexDirection="row"
justifyContent="space-between"
sx={{ paddingLeft: '20px', paddingRight: '217px', marginBottom: '11px', marginTop: '36px' }}
sx={{ paddingLeft: '20px', paddingRight: '206px', marginBottom: '11px', marginTop: '36px' }}
>
<GTSpan></GTSpan>
{matches && <GTSpan></GTSpan>}

View File

@@ -1,16 +1,11 @@
import React from 'react';
import { Box, CardContent, Grid, Typography } from '@mui/material';
import { Box, CardContent, Typography } from '@mui/material';
import { CardWrapper } from '@/components/list/CardListWrapper';
function ImgCardList(props) {
const { data, minWidth, selectedType, setSelectedType } = props;
const { data, selectedType, handleTypeClick } = props;
const srcUrl = '/static/images/';
const handleClick = item => {
console.log('database : ', item);
setSelectedType(item);
};
return (
<Box
component="ul"
@@ -28,7 +23,7 @@ function ImgCardList(props) {
const selected = selectedType && selectedType.id === item.id;
return (
<Box component="li" key={item.id}>
<CardWrapper sx={{ p: 0 }} selected={selected} onClick={() => handleClick(item)}>
<CardWrapper sx={{ p: 0 }} selected={selected} onClick={() => handleTypeClick(item)}>
<CardContent
sx={{
display: 'flex',

View File

@@ -1,5 +1,5 @@
import React from 'react';
import { Box, CardContent, Grid, Stack, Typography } from '@mui/material';
import { Box, CardContent, Typography } from '@mui/material';
import { CardWrapper } from '@/components/list/CardListWrapper';
function LargeImgCardList(props) {
@@ -24,49 +24,52 @@ function LargeImgCardList(props) {
m: '0',
}}
>
{data.map(item => {
const selected = selectedType && selectedType.id === item.id;
return (
<Box component="li" key={item.id}>
<CardWrapper sx={{ width: 169, p: 0 }} selected={selected} onClick={() => handleClick(item)}>
<CardContent
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
width: 169,
height: 166,
pt: '24px',
px: '15px',
}}
>
<Box
component="img"
src={srcUrl + item.icon}
sx={{ width: 48, height: 48, objectFit: 'contain', mb: '12px', border: 0 }}
/>
<Typography
variant="subtitle2"
component="span"
sx={{ textAlign: 'center', lineHeight: '1.3', fontWeight: 'bold' }}
{data
.filter(item => item.seq !== null)
.sort((a, b) => a.seq - b.seq)
.map(item => {
const selected = selectedType && selectedType.id === item.id;
return (
<Box component="li" key={item.id}>
<CardWrapper sx={{ width: 169, p: 0 }} selected={selected} onClick={() => handleClick(item)}>
<CardContent
sx={{
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
width: 169,
height: 166,
pt: '24px',
px: '15px',
}}
>
{item.title}
</Typography>
{item.description ? (
<Box
component="img"
src={srcUrl + item.icon}
sx={{ width: 48, height: 48, objectFit: 'contain', mb: '12px', border: 0 }}
/>
<Typography
variant="caption"
sx={{ mt: '4px', textAlign: 'center', lineHeight: '1.3', color: theme => theme.palette.grey.A700 }}
variant="subtitle2"
component="span"
sx={{ textAlign: 'center', lineHeight: '1.3', fontWeight: 'bold' }}
>
{item.description}
{item.title}
</Typography>
) : (
' '
)}
</CardContent>
</CardWrapper>
</Box>
);
})}
{item.description ? (
<Typography
variant="caption"
sx={{ mt: '4px', textAlign: 'center', lineHeight: '1.3', color: theme => theme.palette.grey.A700 }}
>
{item.description}
</Typography>
) : (
' '
)}
</CardContent>
</CardWrapper>
</Box>
);
})}
</Box>
);
}

View File

@@ -10,7 +10,7 @@ function PageTitleBox(props) {
minWidth: '900px',
paddingLeft: '25px',
paddingRight: '25px',
width: '1920px',
width: '100%',
height: '100%',
};

View File

@@ -5,7 +5,7 @@ import DialogContent from '@mui/material/DialogContent';
import DialogContentText from '@mui/material/DialogContentText';
import DialogTitle from '@mui/material/DialogTitle';
import Button from '@mui/material/Button';
import { Divider } from '@mui/material';
import { Alert, Divider, Snackbar } from '@mui/material';
interface IProps {
message: string | JSX.Element;
@@ -22,11 +22,23 @@ interface IProps {
const buttonStyle = { minWidth: 80, height: 36, padding: '0 10px', fontSize: '13px', fontWeight: 'bold' };
const AlertDialog = ({ close, message, options }: IProps) => {
export const SnackbarTemplate = props => {
const { close, message, options } = props;
console.log(props);
return (
<Snackbar open autoHideDuration={6000} onClose={close}>
<Alert onClose={close} severity={options.type} sx={{ width: '100%' }}>
{message}
</Alert>
</Snackbar>
);
};
const AlertTemplate = ({ close, message, options }: IProps) => {
const hasTitle = options.title && options.title.toString().trim() !== '';
return (
<Dialog
open
open={true}
onClose={close}
keepMounted
aria-labelledby="alert-dialog-slide-title"
@@ -69,6 +81,7 @@ const AlertDialog = ({ close, message, options }: IProps) => {
action.onClick();
close();
}}
autoFocus={true}
variant="contained"
color="primary"
sx={buttonStyle}
@@ -77,7 +90,7 @@ const AlertDialog = ({ close, message, options }: IProps) => {
{action.copy}
</Button>
))}
<Button variant="contained" color="primary" sx={buttonStyle} onClick={close}>
<Button variant="contained" color="primary" sx={buttonStyle} onClick={close} autoFocus={true}>
{options.closeCopy || '확인'}
</Button>
</DialogActions>
@@ -85,4 +98,4 @@ const AlertDialog = ({ close, message, options }: IProps) => {
);
};
export default AlertDialog;
export default AlertTemplate;

View File

@@ -38,6 +38,9 @@ export const CardWrapper = ({ children, selected, onClick, sx = null }) => {
cursor: 'pointer',
position: 'relative',
alignItems: 'center',
'&:hover': {
backgroundColor: '#ebfbff',
},
...sx,
}}
onClick={onClick}

View File

@@ -1,5 +1,5 @@
import React, { Fragment } from 'react';
import { Card, CardActionArea, CardActions, CardContent, Grid, Typography } from '@mui/material';
import { CardActions, CardContent, Grid, Typography } from '@mui/material';
import { Link as RouterLink } from 'react-router-dom';
import CardListWrapper, { CardWrapper } from '@/components/list/CardListWrapper';
import { any } from 'prop-types';
@@ -31,7 +31,7 @@ export const DatabaseCardList = props => {
variant="subtitle2"
sx={{
pl: '10px',
width: disabledIcons ? '100%' : '40%',
width: disabledIcons ? '100%' : '70%',
textOverflow: 'ellipsis',
overflow: 'hidden',
whiteSpace: 'nowrap',

View File

@@ -0,0 +1,24 @@
import React, { createContext } from 'react';
import AlertTemplate, { SnackbarTemplate } from '@/components/alert';
import { positions, Provider, transitions } from 'react-alert';
export const SnackbarContext = createContext(null);
export const AlertProvider = ({ children }) => {
const defaultOp = {
position: positions.BOTTOM_CENTER,
offset: '30px',
transition: transitions.SCALE,
};
const snackbarOp = {
position: positions.BOTTOM_LEFT,
timeout: 6000,
};
return (
<Provider template={AlertTemplate} {...defaultOp}>
<Provider template={SnackbarTemplate} context={SnackbarContext} {...snackbarOp}>
{children}
</Provider>
</Provider>
);
};

View File

@@ -0,0 +1,37 @@
import { createContext, useContext, useState } from 'react';
const AuthContext = createContext(null);
export const AuthProvider = ({ children }) => {
const [token, setToken] = useState(null);
const storage = window.sessionStorage;
const handleLogin = async (id, pwd) => {
return new Promise((resolve, reject) => {
if (id === process.env.REACT_APP_ID && pwd === process.env.REACT_APP_PWD) {
setToken(process.env.REACT_APP_TOKEN);
storage.setItem('loggedUserId', id);
storage.setItem('loggedUserPwd', pwd);
setTimeout(() => resolve(process.env.REACT_APP_TOKEN), 1000);
} else {
reject(new Error('ID 또는 비밀번호가 일치하지 않습니다.'));
}
});
};
const handleLogout = () => {
setToken(null);
};
const value = {
token,
onLogin: handleLogin,
onLogout: handleLogout,
};
return <AuthContext.Provider value={value}>{children}</AuthContext.Provider>;
};
export const useAuth = () => {
return useContext(AuthContext);
};

File diff suppressed because it is too large Load Diff

6
frontend-web/src/global.d.ts vendored Normal file
View File

@@ -0,0 +1,6 @@
declare module '*.jpg' {
export default '' as string;
}
declare module '*.png' {
export default '' as string;
}

View File

@@ -5,7 +5,7 @@ const API_URL = process.env.REACT_APP_API_URL;
const instance = axios.create({
baseURL: API_URL,
timeout: 10000,
timeout: 100000,
// withCredentials: true,
});

View File

@@ -1,35 +1,27 @@
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import theme from './theme/theme';
import { ThemeProvider } from '@mui/material/styles';
import { BrowserRouter } from 'react-router-dom';
import { positions, Provider as AlertProvider, transitions } from 'react-alert';
import AlertTemplate from './components/alert';
import theme from './theme/theme';
import App from './App';
import './index.css';
import { LayoutProvider } from '@/contexts/LayoutContext';
import { LoadingProvider } from '@/contexts/LoadingContext';
// alert optional configuration
const options = {
// you can also just use 'bottom center'
position: positions.BOTTOM_CENTER,
offset: '30px',
// you can also just use 'scale'
transition: transitions.SCALE,
};
import { AuthProvider } from '@/contexts/AuthContext';
import { AlertProvider } from '@/contexts/AlertContext';
import './index.css';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<LayoutProvider>
<LoadingProvider>
<BrowserRouter>
<ThemeProvider theme={theme}>
<AlertProvider template={AlertTemplate} {...options}>
<App />
</AlertProvider>
</ThemeProvider>
<AuthProvider>
<ThemeProvider theme={theme}>
<AlertProvider>
<App />
</AlertProvider>
</ThemeProvider>
</AuthProvider>
</BrowserRouter>
</LoadingProvider>
</LayoutProvider>,

View File

@@ -1,28 +1,34 @@
import React from 'react';
import { Stack, Typography } from '@mui/material';
import { Link, Stack, Typography } from '@mui/material';
const Footer = props => {
const { height } = props;
return (
<Stack sx={{ height: height, alignItems: 'center', justifyContent: 'center' }}>
<Typography
sx={{
width: '100%',
height: '16px',
fontFamily: 'Pretendard',
fontSize: '13px',
fontWeight: 'normal',
fontStretch: 'normal',
fontStyle: 'normal',
lineHeight: 'normal',
letterSpacing: 'normal',
textAlign: 'center',
color: '#767676',
}}
<Link
color="inherit"
href="https://vanillabrain.com/"
sx={{ fontSize: '13px', color: '#767676', textDecoration: 'none' }}
>
@ Vanilla Meta 2022
</Typography>
<Typography
sx={{
width: '100%',
height: '16px',
fontFamily: 'Pretendard',
fontSize: '13px',
fontWeight: 'normal',
fontStretch: 'normal',
fontStyle: 'normal',
lineHeight: 'normal',
letterSpacing: 'normal',
textAlign: 'center',
color: '#767676',
}}
>
@ Vanilla Meta 2022
</Typography>
</Link>
</Stack>
);
};

View File

@@ -1,14 +1,15 @@
import React from 'react';
import { Box, Link } from '@mui/material';
import { Box } from '@mui/material';
import { Link as RouterLink } from 'react-router-dom';
import { ReactComponent as IconLogo } from '@/assets/images/logo.svg';
function Logo(props) {
const { sx = { width: 105, height: 50 } } = props;
return (
<Box sx={sx}>
<Link href="/">
<RouterLink to="/">
<IconLogo style={{ width: '100%', height: '100%' }} />
</Link>
</RouterLink>
</Box>
);
}

View File

@@ -4,8 +4,10 @@ import { Box, Stack } from '@mui/material';
import Header from './Header/Header';
import Footer from './Footer/Footer';
import { LayoutContext } from '@/contexts/LayoutContext';
import { Outlet } from 'react-router-dom';
function Layout(props) {
const Layout = props => {
const { children } = props;
const headerHeight = 65;
const footerHeight = 50;
@@ -33,11 +35,11 @@ function Layout(props) {
minHeight: `calc(100% - ${footerHeight}px)`,
}}
>
{props.children}
{children || <Outlet />}
</Stack>
<Footer height={footerHeight} />
</Box>
);
}
};
export default Layout;

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useRef, useState } from 'react';
import React, { useContext, useEffect, useRef, useState } from 'react';
import {
Avatar,
Button,
@@ -17,23 +17,30 @@ import WidgetService from '@/api/widgetService';
import { STATUS } from '@/constant';
import { useAlert } from 'react-alert';
import CloseButton from '@/components/button/CloseButton';
import { LoadingContext } from '@/contexts/LoadingContext';
function AddWidgetPopup({ label, useWidgetIds = [], widgetOpen = false, widgetSelect = null }) {
const [open, setOpen] = useState(widgetOpen);
const [selectedIds, setSelectedIds] = useState([]);
const [loadedWidgetData, setLoadedWidgetData] = useState([]);
const { showLoading, hideLoading } = useContext(LoadingContext);
const alert = useAlert();
const getItems = () => {
WidgetService.selectWidgetList().then(response => {
if (response.data.status == STATUS.SUCCESS) {
const widgetList = response.data.data.filter(item => {
return !useWidgetIds.find(useItem => useItem == item.id);
});
setLoadedWidgetData(widgetList);
} else {
console.log('조회실패!!!');
}
});
showLoading();
WidgetService.selectWidgetList()
.then(response => {
if (response.data.status == STATUS.SUCCESS) {
const widgetList = response.data.data.filter(item => {
return !useWidgetIds.find(useItem => useItem == item.id);
});
setLoadedWidgetData(widgetList);
} else {
console.log('조회실패!!!');
}
})
.finally(() => {
hideLoading();
});
};
const descriptionElementRef = useRef<HTMLElement>(null);
@@ -93,7 +100,7 @@ function AddWidgetPopup({ label, useWidgetIds = [], widgetOpen = false, widgetSe
}
setOpen(false);
} else {
alert.info('위젯을 선택세요.');
alert.info('위젯을 선택해주세요.');
}
};
@@ -229,7 +236,7 @@ function AddWidgetPopup({ label, useWidgetIds = [], widgetOpen = false, widgetSe
>
</Button>
<span style={{ width: '4px' }}></span>
<span style={{ width: '4px' }} />
<Button
onClick={() => handleSelect()}
sx={{

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import {
Avatar,
Box,
@@ -32,6 +32,7 @@ import { ReactComponent as TemplateIcon08 } from '@/assets/images/template/templ
import { ReactComponent as TemplateIcon09 } from '@/assets/images/template/template09.svg';
import { ReactComponent as TemplateIcon10 } from '@/assets/images/template/template10.svg';
import { ReactComponent as CheckIcon } from '@/assets/images/icon/ic-check.svg';
import { LoadingContext } from '@/contexts/LoadingContext';
const getTemplateIcon = id => {
let icon = null;
@@ -79,6 +80,7 @@ export const WidgetList = ({
const alert = useAlert();
const [loadedWidgetData, setLoadedWidgetData] = useState([]);
const [selectedIds, setSelectedIds] = useState(selectedWidgetIds);
const { showLoading, hideLoading } = useContext(LoadingContext);
useEffect(() => {
getItems();
@@ -93,13 +95,18 @@ export const WidgetList = ({
}, [selectedIds]);
const getItems = () => {
WidgetService.selectWidgetList().then(response => {
if (response.data.status == STATUS.SUCCESS) {
setLoadedWidgetData(response.data.data);
} else {
console.log('조회 실패!!!!');
}
});
showLoading();
WidgetService.selectWidgetList()
.then(response => {
if (response.data.status == STATUS.SUCCESS) {
setLoadedWidgetData(response.data.data);
} else {
console.log('조회 실패!!!!');
}
})
.finally(() => {
hideLoading();
});
};
const handleClick = item => {
@@ -140,7 +147,7 @@ export const WidgetList = ({
handleWidgetConfirm(widgets);
}
} else {
alert.info('위젯을 선택세요.');
alert.info('위젯을 선택해주세요.');
}
};
@@ -216,7 +223,7 @@ export const WidgetList = ({
>
</Button>
<span style={{ width: '4px' }}></span>
<span style={{ width: '4px' }} />
<Button
onClick={() => handleConfirmClick()}
sx={{
@@ -275,7 +282,7 @@ export const TemplateList = ({ handleWidgetConfirm = null, handleWidgetCancel =
handleWidgetConfirm(selectedItem);
}
} else {
alert.info('템플릿을 선택세요.');
alert.info('템플릿을 선택해주세요.');
}
};
@@ -337,6 +344,7 @@ export const TemplateList = ({ handleWidgetConfirm = null, handleWidgetCancel =
overflow: 'hidden',
textOverflow: 'ellipsis',
width: '224px',
maxWidth: '100%',
whiteSpace: 'nowrap',
}}
>
@@ -390,7 +398,7 @@ export const TemplateList = ({ handleWidgetConfirm = null, handleWidgetCancel =
>
</Button>
<span style={{ width: '4px' }}></span>
<span style={{ width: '4px' }} />
<Button
onClick={() => handleConfirmClick()}
sx={{
@@ -533,8 +541,8 @@ function RecommendDashboardPopup({ recommendOpen = false, handleComplete = null
position: 'absolute',
right: '0px',
top: '0px',
paddingRight: '19px',
paddingTop: '19px',
marginRight: '12px',
marginTop: '12px',
cursor: 'pointer',
}}
size="medium"

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import { Box, Button, Card, Stack, TextField } from '@mui/material';
import { useNavigate, useSearchParams } from 'react-router-dom';
import PageContainer from '@/components/PageContainer';
@@ -7,6 +7,7 @@ import AddWidgetPopup from '@/pages/Dashboard/Components/AddWidgetPopup';
import ConfirmCancelButton from '@/components/button/ConfirmCancelButton';
import { Responsive, WidthProvider } from 'react-grid-layout';
import { useAlert } from 'react-alert';
import { SnackbarContext } from '@/contexts/AlertContext';
import '/node_modules/react-grid-layout/css/styles.css';
import '/node_modules/react-resizable/css/styles.css';
@@ -18,13 +19,17 @@ import { STATUS } from '@/constant';
import DashboardTitleBox from '../Components/DashboardTitleBox';
import CloseButton from '@/components/button/CloseButton';
import bg from '@/assets/images/dashboard-bg.svg';
import { LoadingContext } from '@/contexts/LoadingContext';
const ResponsiveGridLayout = WidthProvider(Responsive);
function DashboardModify() {
const alert = useAlert();
const [searchParams, setSearchParams] = useSearchParams();
const snackbar = useAlert(SnackbarContext);
const [searchParams] = useSearchParams();
const navigate = useNavigate();
const { showLoading, hideLoading } = useContext(LoadingContext);
const [dashboardId, setDashboardId] = useState(null); // dashboard id
const [dashboardTitle, setDashboardTitle] = useState(''); // dashboard 제목
const [widgets, setWidgets] = useState([]); // widget 정보
@@ -62,22 +67,26 @@ function DashboardModify() {
// dashboard info 조회
const getDashboardInfo = id => {
// todo 서비스 완료시 연결
DashboardService.selectDashboard(id).then(response => {
if (response.data.status == STATUS.SUCCESS) {
setDashboardTitle(response.data.data.title);
setWidgets(response.data.data.widgets);
showLoading();
DashboardService.selectDashboard(id)
.then(response => {
if (response.data.status == STATUS.SUCCESS) {
setDashboardTitle(response.data.data.title);
setWidgets(response.data.data.widgets);
response.data.data.layout.map((item, index) => {
if (item.i !== undefined) {
item.i = item.i.toString();
}
});
setLayout(response.data.data.layout);
} else {
alert.error('조회 실패하였습니다.');
}
});
response.data.data.layout.map(item => {
if (item.i !== undefined) {
item.i = item.i.toString();
}
});
setLayout(response.data.data.layout);
} else {
alert.error('대시보드 조회 실패습니다.');
}
})
.finally(() => {
hideLoading();
});
};
useEffect(() => {
@@ -101,18 +110,86 @@ function DashboardModify() {
setLayout(changeLayout);
};
// 추가 할 layout
// @tempLayout 현재 배치되어 있는 layout 정보
// @x : 새로 배치될 위젯의 x 초기값
// @y : 새로 배치될 위젯의 y 초기값
// @w : 새로 배치될 위젯의 width 값
// @h : 새로 배치될 위젯의 height 값
const getCalculatorPosition = (tempLayout, x = 0, y = 0, w = 6, h = 5, layoutMaxW = 12) => {
const tempPos = { startX: x, endX: w - 1, startY: y, endY: h - 1 }; // layout 계산을 위한 값 보정
tempLayout.sort((a, b) => a.y - b.y || a.x - b.x);
const tmepPosArr = [];
tempLayout.map(item => {
tmepPosArr.push({ startX: item.x, endX: item.x + item.w - 1, startY: item.y, endY: item.y + item.h - 1 }); // layout 계산을 위한 값 보정
});
// layout 위치 계산 (재귀함수로 사용. 해당 함수를 수정할 때 무한 loop 되지 않게 조심할 것)
const calculator = (pos, posArr) => {
const maxW = layoutMaxW - 1; // layout 계산을 위한 값 보정
// 추가할 위젯의 위치를 1칸씩 변경
const getCalculatorPos = pos => {
if (pos.endX + 1 > maxW) {
pos.endX = pos.endX - pos.startX;
pos.startX = 0;
pos.startY = pos.startY + 1;
pos.endY = pos.endY + 1;
} else {
pos.startX = pos.startX + 1;
pos.endX = pos.endX + 1;
}
return pos;
};
for (let i = 0; i < posArr.length; i++) {
const item = posArr[i];
let isCompareHit = false;
if (pos.startX <= item.startX && (pos.endX >= item.startX || pos.endX > item.endX)) {
// x 좌표가 겹치는 상황
isCompareHit = true;
} else if (pos.startX >= item.startX && pos.startX <= item.endX) {
// x 좌표가 겹치는 상황
isCompareHit = true;
}
if (isCompareHit) {
if (pos.startY <= item.startY && (pos.endY >= item.startY || pos.endY > item.endY)) {
// x, y 좌표가 겹치는 상황
return calculator(getCalculatorPos(pos), posArr);
} else if (pos.startY >= item.startY && pos.startY <= item.endY) {
// x, y 좌표가 겹치는 상황
return calculator(getCalculatorPos(pos), posArr);
}
}
}
return pos;
};
const resultPos = calculator(tempPos, tmepPosArr);
// 보정된 layout 값을 원래대로 복원
return {
x: resultPos.startX,
y: resultPos.startY,
w: resultPos.endX - resultPos.startX + 1,
h: resultPos.endY - resultPos.startY + 1,
};
};
// widget 생성
const generateWidget = () => {
useWidgetIds.length = 0;
const addLayouts = [];
widgets.map((item, index) => {
if (layout.length <= index) {
const calculatorPosition = getCalculatorPosition([...layout, ...addLayouts], 0, 0, 6, 5, 12);
addLayouts.push({
x: 0,
y: 0,
w: 5,
h: 5,
i: item.id.toString(),
...calculatorPosition,
});
}
});
@@ -121,7 +198,7 @@ function DashboardModify() {
setLayout([...layout, ...addLayouts]);
}
return widgets.map((item, index) => {
return widgets.map(item => {
useWidgetIds.push(item.id); // 현재 widget id 를 담는다.
return (
<Card
@@ -145,6 +222,7 @@ function DashboardModify() {
paddingRight: '8px',
paddingTop: '8px',
cursor: 'pointer',
zIndex: 2000,
}}
size="medium"
onClick={event => {
@@ -174,16 +252,17 @@ function DashboardModify() {
// title null 체크, widgets 수 체크 (0개면 저장 못함)
if (dashboardTitle == null || dashboardTitle.trim() == '') {
// title 이 없을 경우
alert.info('제목을 입력 해주세요.', {
onClose: () => {
// todo 아래 기능 연결하기
console.log('title 로 포커스 이동하기');
},
});
alert.info('제목을 입력해주세요.');
} else if (layout.length == 0 || widgets.length == 0) {
// 배치된 widget 이 없을경우
alert.info('배치된 위젯이 없습니다.');
} else {
// 저장 전 react grid layout 에서 필요없는 속성 제거
layout.map(item => {
delete item.moved;
delete item.static;
});
// 저장 로직
dashboardInfo.dashboardId = dashboardId;
dashboardInfo.title = dashboardTitle;
@@ -198,9 +277,19 @@ function DashboardModify() {
{
copy: '수정',
onClick: () => {
DashboardService.updateDashboard(dashboardId, dashboardInfo).then(() => {
navigate('/dashboard');
});
showLoading();
DashboardService.updateDashboard(dashboardId, dashboardInfo)
.then(response => {
if (response.data.status === 'SUCCESS') {
navigate('/dashboard/' + dashboardId, { replace: true });
snackbar.success('대시보드가 수정되었습니다.');
} else {
alert.error('대시보드 수정에 실패했습니다.');
}
})
.finally(() => {
hideLoading();
});
},
},
],
@@ -213,9 +302,19 @@ function DashboardModify() {
{
copy: '생성',
onClick: () => {
DashboardService.createDashboard(dashboardInfo).then(() => {
navigate('/dashboard');
});
showLoading();
DashboardService.createDashboard(dashboardInfo)
.then(response => {
if (response.data.status === 'SUCCESS') {
navigate('/dashboard');
snackbar.success('대시보드가 생성되었습니다.');
} else {
alert.error('대시보드 생성에 실패했습니다.');
}
})
.finally(() => {
hideLoading();
});
},
},
],
@@ -225,7 +324,7 @@ function DashboardModify() {
};
// 취소 여부 버튼 이벤트
const handleCancelDialogSelect = detail => {
const handleCancelDialogSelect = () => {
navigate(-1);
};
@@ -237,7 +336,7 @@ function DashboardModify() {
console.log(dashboardInfo);
setWidgets(dashboardInfo.widgets);
dashboardInfo.layout.map((item, index) => {
dashboardInfo.layout.map(item => {
if (item.i !== undefined) {
item.i = item.i.toString();
}
@@ -266,7 +365,7 @@ function DashboardModify() {
title={
<TextField
id="userDashboardName"
label=""
label="대시보드 이름"
required
sx={{
width: '960px',

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import { Box, Card, Stack, Typography } from '@mui/material';
import { Link as RouterLink, useNavigate, useParams } from 'react-router-dom';
import PageTitleBox from '@/components/PageTitleBox';
@@ -13,6 +13,8 @@ import DashboardTitleBox from '../Components/DashboardTitleBox';
import ModifyButton from '@/components/button/ModifyButton';
import DeleteButton from '@/components/button/DeleteButton';
import ReloadButton from '@/components/button/ReloadButton';
import { SnackbarContext } from '@/contexts/AlertContext';
import { LoadingContext } from '@/contexts/LoadingContext';
const ResponsiveGridLayout = WidthProvider(Responsive);
@@ -20,7 +22,9 @@ const DashboardView = () => {
const { dashboardId } = useParams();
const navigate = useNavigate();
const alert = useAlert();
const snackbar = useAlert(SnackbarContext);
const { showLoading, hideLoading } = useContext(LoadingContext);
const [dashboardInfo, setDashboardInfo] = useState({ title: '', widgets: [], layout: [], updatedAt: '' }); // dashboard 정보
const [layout, setLayout] = useState([]); // grid layout
// dashboard id
@@ -32,7 +36,7 @@ const DashboardView = () => {
// dashboardInfo useEffect
useEffect(() => {
dashboardInfo.layout.map((item, index) => {
dashboardInfo.layout.map(item => {
if (item.i !== undefined) {
item.i = item.i.toString();
}
@@ -44,13 +48,18 @@ const DashboardView = () => {
// dashboard info 조회
const getDashboardInfo = id => {
DashboardService.selectDashboard(id).then(response => {
if (response.data.status == STATUS.SUCCESS) {
setDashboardInfo(response.data.data);
} else {
alert.error('조회 실패하였습니다.');
}
});
showLoading();
DashboardService.selectDashboard(id)
.then(response => {
if (response.data.status == STATUS.SUCCESS) {
setDashboardInfo(response.data.data);
} else {
alert.error('대시보드 조회에 실패했습니다.');
}
})
.finally(() => {
hideLoading();
});
};
const dateData = data => {
@@ -72,7 +81,7 @@ const DashboardView = () => {
// widget 생성
const generateWidget = () => {
return dashboardInfo.widgets.map((item, index) => {
return dashboardInfo.widgets.map(item => {
return (
<Card
key={item.id}
@@ -92,23 +101,25 @@ const DashboardView = () => {
};
const handleDeleteSelect = () => {
alert.success(dashboardInfo.title + '\n삭제하겠습니까?', {
alert.success(dashboardInfo.title + '\n대시보드를 삭제하겠습니까?', {
closeCopy: '취소',
actions: [
{
copy: '확인',
onClick: () => {
DashboardService.deleteDashboard(dashboardId).then(response => {
if (response.data.status == STATUS.SUCCESS) {
alert.info('삭제되었습니다.', {
onClose: () => {
navigate('/dashboard', { replace: true });
},
});
} else {
alert.info('삭제 실패하였습니다.');
}
});
showLoading();
DashboardService.deleteDashboard(dashboardId)
.then(response => {
if (response.data.status == STATUS.SUCCESS) {
navigate('/dashboard', { replace: true });
snackbar.success('대시보드가 삭제되었습니다.');
} else {
alert.error('대시보드 삭제에 실패했습니다.');
}
})
.finally(() => {
hideLoading();
});
},
},
],

View File

@@ -1,4 +1,4 @@
import React, { useEffect, useState } from 'react';
import React, { useContext, useEffect, useState } from 'react';
import PageContainer from '@/components/PageContainer';
import PageTitleBox from '@/components/PageTitleBox';
import BoardList from '@/components/BoardList';
@@ -10,17 +10,19 @@ import DashboardService from '@/api/dashboardService';
import { STATUS } from '@/constant';
import { useAlert } from 'react-alert';
import { styled } from '@mui/system';
import { LoadingContext } from '@/contexts/LoadingContext';
import { SnackbarContext } from '@/contexts/AlertContext';
const title = '대시보드';
function Dashboard() {
const { dashboardId } = useParams();
const alert = useAlert();
const snackbar = useAlert(SnackbarContext);
const navigate = useNavigate();
const [isLoading, setIsLoading] = useState(false);
const [loadedDashboardData, setLoadedDashboardData] = useState([]);
const [loadedCount, setLoadedCount] = useState(1);
const [noData, setNoData] = useState(false);
const { showLoading, hideLoading } = useContext(LoadingContext);
const GTSpan = styled('span')({
fontFamily: 'Pretendard',
@@ -41,41 +43,46 @@ function Dashboard() {
useEffect(() => {
getDashboardList();
setIsLoading(true);
}, []);
// dashboard info 조회
const getDashboardList = () => {
DashboardService.selectDashboardList().then(response => {
if (response.data.status == STATUS.SUCCESS) {
setLoadedDashboardData(response.data.data);
setNoData(response.data.data.length == 0);
} else {
alert.error('서비스 실패!');
}
});
setIsLoading(true);
showLoading();
DashboardService.selectDashboardList()
.then(response => {
if (response.data.status == STATUS.SUCCESS) {
setLoadedDashboardData(response.data.data);
setNoData(response.data.data.length == 0);
} else {
alert.error('대시보드 조회에 실패했습니다.');
}
})
.finally(() => {
hideLoading();
});
};
const handleDeleteSelect = item => {
console.log(item);
alert.success(item.title + '\n삭제하겠습니까?', {
alert.success(item.title + '\n대시보드를 삭제하겠습니까?', {
closeCopy: '취소',
actions: [
{
copy: '확인',
onClick: () => {
DashboardService.deleteDashboard(item.id).then(response => {
if (response.data.status == STATUS.SUCCESS) {
alert.info('삭제되었습니다.', {
onClose: () => {
getDashboardList();
},
});
} else {
alert.error('삭제 실패하였습니다.');
}
});
showLoading();
DashboardService.deleteDashboard(item.id)
.then(response => {
if (response.data.status == STATUS.SUCCESS) {
getDashboardList();
snackbar.success('대시보드가 삭제되었습니다.');
} else {
alert.error('대시보드 삭제에 실패했습니다.');
}
})
.finally(() => {
hideLoading();
});
},
},
],

View File

@@ -9,6 +9,7 @@ import DatasetService from '@/api/datasetService';
import AddButton from '@/components/button/AddButton';
import { Link as RouterLink } from 'react-router-dom';
import { LoadingContext } from '@/contexts/LoadingContext';
import { SnackbarContext } from '@/contexts/AlertContext';
const DataLayout = props => {
const { isViewMode, setDataSet } = props;
@@ -16,6 +17,7 @@ const DataLayout = props => {
const [datasetList, setDatasetList] = useState([]);
const [tableList, setTableList] = useState([]);
const alert = useAlert();
const snackbar = useAlert(SnackbarContext);
const { showLoading, hideLoading } = useContext(LoadingContext);
const [selectedDatabase, setSelectedDatabase] = useState({
@@ -57,10 +59,17 @@ const DataLayout = props => {
showLoading();
DatabaseService.selectDatabase(selectedDatabase.databaseId)
.then(response => {
setDatasetList(response.data.data.datasets);
setTableList(response.data.data.tables);
if (response.data.status === 'SUCCESS') {
setDatasetList(response.data.data.datasets);
setTableList(response.data.data.tables);
} else {
alert.error('데이터베이스 조회에 실패했습니다.');
setDatasetList([]);
setTableList([]);
}
})
.catch(() => {
.catch(error => {
snackbar.error(error.message);
setDatasetList([]);
setTableList([]);
})
@@ -81,6 +90,7 @@ const DataLayout = props => {
DatabaseService.deleteDatabase(id).then(response => {
if (response.data.status === STATUS.SUCCESS) {
getDatabaseList();
snackbar.success('데이터베이스가 삭제되었습니다.');
}
});
},
@@ -97,15 +107,16 @@ const DataLayout = props => {
const handleDeleteDataset = item => {
console.log('handleDeleteDataset', item);
alert.success(`${item.title}\n데이터셋 삭제하시겠습니까?`, {
alert.success(`${item.title}\n데이터셋 삭제하시겠습니까?`, {
title: '데이터베이스 삭제',
closeCopy: '취소',
actions: [
{
copy: '삭제',
onClick: () => {
DatasetService.deleteDataset(item.id).then(response => {
DatasetService.deleteDataset(item.id).then(() => {
getDatabaseInfo();
snackbar.success('데이터셋이 삭제되었습니다.');
});
},
},

View File

@@ -1,5 +1,5 @@
import React, { useContext, useEffect, useLayoutEffect, useState } from 'react';
import { MenuItem, Select, Stack, TextField } from '@mui/material';
import { MenuItem, Select, Stack, TextField, Typography } from '@mui/material';
import { useAlert } from 'react-alert';
import PageTitleBox from '@/components/PageTitleBox';
import SubmitButton from '@/components/button/SubmitButton';
@@ -16,6 +16,7 @@ import { useLocation, useNavigate, useParams } from 'react-router-dom';
import { STATUS } from '@/constant';
import { getDatabaseIcon } from '@/widget/utils/iconUtil';
import { LoadingContext } from '@/contexts/LoadingContext';
import { SnackbarContext } from '@/contexts/AlertContext';
const DataSet = () => {
const { setId, sourceId } = useParams();
@@ -26,20 +27,19 @@ const DataSet = () => {
const [isModifyMode, setIsModifyMode] = useState(false);
const [testCompleted, setTestCompleted] = useState(false);
const [datasetInfo, setDatasetInfo] = useState({ databaseId: sourceId, title: '', query: '' });
// 'select A.*, C.bizId from UserInfo A, BizUserMap B, BizInfo C where A.userId = B.userId and B.bizId = C.bizId',
const [data, setData] = useState([]);
const [columns, setColumns] = useState([]);
const [databaseId, setDatabaseId] = useState(null);
const [databaseList, setDatabaseList] = useState([]);
const [tableList, setTableList] = useState([]);
const alert = useAlert();
// console.log(data, 'data', sourceId, 'sourceId', databaseId, 'databaseId', setId, 'setId');
const alert = useAlert();
const snackbar = useAlert(SnackbarContext);
useLayoutEffect(() => {
console.log('modify', pathname.indexOf('modify'));
console.log('setId', setId);
console.log('sourceId', sourceId);
console.log('setId:', setId, 'sourceId:', sourceId);
getDatabaseTypeList();
if (pathname.indexOf('modify') > 0 && setId) {
@@ -57,6 +57,10 @@ const DataSet = () => {
addCompleter();
}, [tableList]);
useEffect(() => {
if (!databaseId) getDatabaseId();
}, [databaseList, datasetInfo]);
/**
* 데이터 그리드 컬럼 생성
* @param data
@@ -68,10 +72,9 @@ const DataSet = () => {
} else if (data instanceof Object) {
target = data;
}
const columns = Object.keys(target).map(key => {
return Object.keys(target).map(key => {
return { name: key, header: key, align: key, width: 200, sortable: true };
});
return columns;
};
const addCompleter = () => {
@@ -107,7 +110,7 @@ const DataSet = () => {
};
const onChangeDatabaseId = event => {
console.log('onChaonChangeDatabaseIdngeTitle', event.target.value);
console.log('changeDatabaseId', event.target.value);
setDatabaseId(event.target.value);
setDatasetInfo(prevState => ({ ...prevState, databaseId: event.target.value }));
};
@@ -116,28 +119,45 @@ const DataSet = () => {
* 데이터베이스 타입 목록 조회
*/
const getDatabaseTypeList = () => {
DatabaseService.selectDatabaseList().then(response => {
console.log('selectDatabaseTypeList', response.data);
if (response.data.status === STATUS.SUCCESS) {
const list = response.data.data;
list.map(item => (item.icon = getDatabaseIcon(item.engine)));
setDatabaseList(list);
if (!isModifyMode && list.length > 0) {
// setDatabaseId(list[0].id);
setDatabaseId(sourceId);
showLoading();
DatabaseService.selectDatabaseList()
.then(response => {
console.log('selectDatabaseTypeList', response.data);
if (response.data.status === STATUS.SUCCESS) {
const list = response.data.data;
list.map(item => (item.icon = getDatabaseIcon(item.engine)));
setDatabaseList(list);
}
})
.finally(() => {
hideLoading();
});
};
const getDatabaseId = () => {
if (databaseList.length > 0) {
if (!isModifyMode) {
setDatabaseId(sourceId);
} else {
setDatabaseId(datasetInfo?.databaseId);
}
});
}
};
const getDatabaseInfo = () => {
showLoading();
DatabaseService.selectDatabase(databaseId)
.then(response => {
setTableList(response.data.data.tables);
console.log('tableList ', response.data.data.tables);
if (response.data.status === 'SUCCESS') {
setTableList(response.data.data.tables);
console.log('tableList ', response.data.data.tables);
} else {
alert.error('데이터베이스 조회에 실패했습니다.');
setTableList([]);
}
})
.catch(() => {
.catch(error => {
snackbar.error(error.message);
setTableList([]);
})
.finally(() => {
@@ -149,14 +169,19 @@ const DataSet = () => {
* 수정일 경우 데이터셋 정보 조회
*/
const getDatasetInfo = () => {
DatasetService.selectDataset(setId).then(response => {
console.log('selectDataset', response.data.data.datasetId);
if (response.data.status === 'SUCCESS') {
setDatasetInfo(response.data.data);
setDatabaseId(response.data.data.databaseId);
// setDataType(list[0]);
}
});
showLoading();
DatasetService.selectDataset(setId)
.then(response => {
console.log('selectDataset', response.data.data.id, response.data.data.databaseId);
if (response.data.status === 'SUCCESS') {
setDatasetInfo(response.data.data);
} else {
alert.error('데이터베이스 조회에 실패했습니다.');
}
})
.finally(() => {
hideLoading();
});
};
/**
@@ -164,9 +189,6 @@ const DataSet = () => {
*/
const excuteQuery = () => {
showLoading();
if (datasetInfo.query.trim().length < 1) {
alert.info('쿼리를 입력해주세요.');
}
const param = {
id: databaseId,
query: datasetInfo.query,
@@ -176,15 +198,22 @@ const DataSet = () => {
.then(response => {
console.log(response.data);
if (response.data.status === 'SUCCESS') {
setData(response.data.datas);
setTestCompleted(true);
setData(response.data.datas);
setColumns(createColumns(response.data.datas));
snackbar.success('Success!');
} else {
setTestCompleted(false);
setData([]);
setColumns([]);
snackbar.error(`${response.data.message}`);
}
})
.catch(() => {
.catch(error => {
snackbar.error(error.message);
setTestCompleted(false);
setData([]);
setColumns([]);
})
.finally(() => {
hideLoading();
@@ -201,28 +230,35 @@ const DataSet = () => {
{
copy: '확인',
onClick: () => {
showLoading();
if (isModifyMode) {
DatasetService.updateDataset(setId, datasetInfo).then(response => {
console.log(response.data);
if (response.data.status === STATUS.SUCCESS) {
alert.info('데이터셋이 수정되었습니다.', {
onClose: () => {
navigate('/data');
},
});
}
});
DatasetService.updateDataset(setId, datasetInfo)
.then(response => {
console.log(response.data);
if (response.data.status === STATUS.SUCCESS) {
navigate('/data');
snackbar.success('데이터셋이 수정되었습니다.');
} else {
alert.error('데이터셋 수정에 실패했습니다.');
}
})
.finally(() => {
hideLoading();
});
} else {
DatasetService.createDataset(datasetInfo).then(response => {
console.log(response.data);
if (response.data.status === STATUS.SUCCESS) {
alert.info('데이터셋이 생성되었습니다.', {
onClose: () => {
navigate('/data');
},
});
}
});
DatasetService.createDataset(datasetInfo)
.then(response => {
console.log(response.data);
if (response.data.status === STATUS.SUCCESS) {
navigate('/data');
snackbar.success('데이터셋이 생성되었습니다.');
} else {
alert.error('데이터셋 생성에 실패했습니다.');
}
})
.finally(() => {
hideLoading();
});
}
},
},
@@ -273,7 +309,15 @@ const DataSet = () => {
displayEmpty
disabled={isModifyMode}
size="small"
value={databaseId || ''}
value={databaseId ?? ''}
// renderValue={selected => {
// if (selected.length === 0) {
// return <Typography sx={{ color: '#929292', fontStyle: 'italic' }}>데이터베이스를 선택해 주세요.</Typography>;
// } else {
// const item = databaseList?.find(({ id: value }) => value === selected);
// return item?.name;
// }
// }}
onChange={onChangeDatabaseId}
>
{databaseList.map(item => (
@@ -290,7 +334,6 @@ const DataSet = () => {
value={datasetInfo?.title}
onChange={onChangeTitle}
required
// helperText="데이터셋의 이름을 입력해 주세요"
/>
<AceEditor
placeholder="Please enter a query."
@@ -312,19 +355,29 @@ const DataSet = () => {
tabSize: 2,
}}
/>
<SubmitButton label="Run" type="button" sx={{ width: '374px', height: '50px' }} onClick={excuteQuery} />
</Stack>
<Stack sx={{ p: '30px 25px 40px 25px', backgroundColor: '#f5f6f8' }}>
<DataGrid
minBodyHeight={300}
bodyHeight={500}
data={data}
columns={columns}
columnOptions={{
resizable: true,
}}
<SubmitButton
label="Run"
type="button"
size="large"
sx={{ width: '374px', fontSize: '13px' }}
onClick={excuteQuery}
/>
</Stack>
{data.length ? (
<Stack sx={{ p: '30px 25px 40px 25px', backgroundColor: '#f5f6f8' }}>
<DataGrid
minBodyHeight={300}
bodyHeight={500}
data={data}
columns={columns}
columnOptions={{
resizable: true,
}}
/>
</Stack>
) : (
''
)}
</PageTitleBox>
);
};

View File

@@ -0,0 +1,78 @@
import React from 'react';
import { Stack, TextField } from '@mui/material';
import SubmitButton from '@/components/button/SubmitButton';
const inputStyle = {
width: '800px',
};
const BigQueryDatabaseForm = props => {
const { testConnect, formData, setFormData } = props;
const handleSubmit = data => {
data.preventDefault();
const item = {
name: data.target.name.value,
projectId: data.target.projectId.value,
keyFilename: data.target.keyFilename.value,
schema: data.target.schema.value,
};
testConnect(item);
};
const handleNameChange = event => {
setFormData(prevState => ({ ...prevState, [event.target.name]: event.target.value }));
};
const handleChange = event => {
setFormData(prevState => ({
...prevState,
bigquery: { ...prevState.bigquery, [event.target.name]: event.target.value },
}));
};
return (
<Stack component="form" sx={{ maxWidth: 800, mx: 'auto', mt: 3 }} onSubmit={handleSubmit}>
<Stack sx={{ display: 'flex', justifyContent: 'space-between' }} spacing="20px">
<TextField
label="이름"
name="name"
value={formData?.name || ''}
required
fullWidth
sx={inputStyle}
onChange={handleNameChange}
/>
<TextField
label="Project Id"
name="projectId"
value={formData?.bigquery?.projectId || ''}
required
fullWidth
sx={inputStyle}
onChange={handleChange}
/>
<TextField
label="File Name"
name="keyFilename"
value={formData?.bigquery?.keyFilename || ''}
required
fullWidth
sx={inputStyle}
onChange={handleChange}
/>
<TextField
label="Schema"
name="schema"
value={formData?.bigquery?.schema || ''}
required
fullWidth
sx={inputStyle}
onChange={handleChange}
/>
<SubmitButton label="TEST CONNECT" type="submit" sx={{ height: '50px', fontSize: '13px' }} />
</Stack>
</Stack>
);
};
export default BigQueryDatabaseForm;

View File

@@ -8,14 +8,12 @@ const inputStyle = {
const DatabaseForm = props => {
const { testConnect, formData, setFormData } = props;
console.log('formData', formData);
const handleSubmit = data => {
console.log('DatabaseForm');
data.preventDefault();
const item = {
name: data.target.name.value,
host: data.target.host.value,
port: data.target.port.value,
port: Number(data.target.port.value),
user: data.target.user.value,
password: data.target.password.value,
database: data.target.database.value,
@@ -23,26 +21,30 @@ const DatabaseForm = props => {
testConnect(item);
};
const handleChange = event => {
const handleNameChange = event => {
setFormData(prevState => ({ ...prevState, [event.target.name]: event.target.value }));
};
const handleChange = event => {
setFormData(prevState => ({ ...prevState, default: { ...prevState.default, [event.target.name]: event.target.value } }));
};
return (
<Stack component="form" sx={{ maxWidth: 800, mx: 'auto', mt: 3 }} onSubmit={handleSubmit}>
<Stack sx={{ display: 'flex', justifyContent: 'space-between' }} spacing="20px">
<TextField
label="이름"
name="databaseName"
value={formData.databaseName}
name="name"
value={formData?.name || ''}
required
fullWidth
sx={inputStyle}
onChange={handleChange}
onChange={handleNameChange}
/>
<TextField
label="Host"
name="host"
value={formData.host}
value={formData?.default?.host || ''}
required
fullWidth
sx={inputStyle}
@@ -51,7 +53,8 @@ const DatabaseForm = props => {
<TextField
label="Port"
name="port"
value={formData.port}
type="number"
value={formData?.default?.port || ''}
required
fullWidth
sx={inputStyle}
@@ -60,7 +63,7 @@ const DatabaseForm = props => {
<TextField
label="User"
name="user"
value={formData.user}
value={formData?.default?.user || ''}
required
fullWidth
sx={inputStyle}
@@ -69,7 +72,7 @@ const DatabaseForm = props => {
<TextField
label="Password"
name="password"
value={formData.password}
value={formData?.default?.password || ''}
required
fullWidth
sx={inputStyle}
@@ -78,13 +81,13 @@ const DatabaseForm = props => {
<TextField
label="Schema"
name="database"
value={formData.database}
value={formData?.default?.database || ''}
required
fullWidth
sx={inputStyle}
onChange={handleChange}
/>
<SubmitButton label="Test Connect" type="submit" />
<SubmitButton label="TEST CONNECT" type="submit" sx={{ height: '50px', fontSize: '13px' }} />
</Stack>
</Stack>
);

View File

@@ -0,0 +1,126 @@
import React from 'react';
import { Stack, TextField } from '@mui/material';
import SubmitButton from '@/components/button/SubmitButton';
const inputStyle = {
width: '800px',
};
const OracleDatabaseForm = props => {
const { testConnect, formData, setFormData } = props;
const handleSubmit = data => {
data.preventDefault();
const item = {
name: data.target.name.value,
host: data.target.host.value,
port: Number(data.target.port.value),
user: data.target.user.value,
password: data.target.password.value,
database: data.target.database.value,
instanceName: data.target.instanceName.value,
fetchAsString: data.target.fetchAsString.value,
requestTimeout: data.target.requestTimeout.value,
};
testConnect(item);
};
const handleNameChange = event => {
setFormData(prevState => ({ ...prevState, [event.target.name]: event.target.value }));
};
const handleChange = event => {
setFormData(prevState => ({ ...prevState, oracle: { ...prevState.oracle, [event.target.name]: event.target.value } }));
};
return (
<Stack component="form" sx={{ maxWidth: 800, mx: 'auto', mt: 3 }} onSubmit={handleSubmit}>
<Stack sx={{ display: 'flex', justifyContent: 'space-between' }} spacing="20px">
<TextField
label="이름"
name="name"
value={formData?.name || ''}
required
fullWidth
sx={inputStyle}
onChange={handleNameChange}
/>
<TextField
label="Host"
name="host"
value={formData?.oracle?.host || ''}
required
fullWidth
sx={inputStyle}
onChange={handleChange}
/>
<TextField
label="Port"
name="port"
type="number"
value={formData?.oracle?.port || ''}
required
fullWidth
sx={inputStyle}
onChange={handleChange}
/>
<TextField
label="User"
name="user"
value={formData?.oracle?.user || ''}
required
fullWidth
sx={inputStyle}
onChange={handleChange}
/>
<TextField
label="Password"
name="password"
value={formData?.oracle?.password || ''}
required
fullWidth
sx={inputStyle}
onChange={handleChange}
/>
<TextField
label="Database"
name="database"
value={formData?.oracle?.database || ''}
required
fullWidth
sx={inputStyle}
onChange={handleChange}
/>
<TextField
label="InstanceName"
name="instanceName"
value={formData?.oracle?.instanceName || ''}
required
fullWidth
sx={inputStyle}
onChange={handleChange}
/>
<TextField
label="FetchAsString"
name="fetchAsString"
value={formData?.oracle?.fetchAsString || ''}
required
fullWidth
sx={inputStyle}
onChange={handleChange}
/>
<TextField
label="RequestTimeout"
name="requestTimeout"
value={formData?.oracle?.requestTimeout || ''}
required
fullWidth
sx={inputStyle}
onChange={handleChange}
/>
<SubmitButton label="TEST CONNECT" type="submit" sx={{ height: '50px', fontSize: '13px' }} />
</Stack>
</Stack>
);
};
export default OracleDatabaseForm;

View File

@@ -0,0 +1,118 @@
import React from 'react';
import { Stack, TextField } from '@mui/material';
import SubmitButton from '@/components/button/SubmitButton';
const inputStyle = {
width: '800px',
};
const SnowflakeDatabaseForm = props => {
const { testConnect, formData, setFormData } = props;
const handleSubmit = data => {
data.preventDefault();
const item = {
name: data.target.name.value,
account: data.target.account.value,
username: data.target.username.value,
password: data.target.password.value,
database: data.target.database.value,
application: data.target.application.value,
schema: data.target.schema.value,
warehouse: data.target.warehouse.value,
};
testConnect(item);
};
const handleNameChange = event => {
setFormData(prevState => ({ ...prevState, [event.target.name]: event.target.value }));
};
const handleChange = event => {
setFormData(prevState => ({
...prevState,
snowflake: { ...prevState.snowflake, [event.target.name]: event.target.value },
}));
};
return (
<Stack component="form" sx={{ maxWidth: 800, mx: 'auto', mt: 3 }} onSubmit={handleSubmit}>
<Stack sx={{ display: 'flex', justifyContent: 'space-between' }} spacing="20px">
<TextField
label="이름"
name="name"
value={formData?.name || ''}
required
fullWidth
sx={inputStyle}
onChange={handleNameChange}
/>
<TextField
label="Account"
name="account"
value={formData?.snowflake?.account || ''}
required
fullWidth
sx={inputStyle}
onChange={handleChange}
/>
<TextField
label="Username"
name="username"
value={formData?.snowflake?.username || ''}
required
fullWidth
sx={inputStyle}
onChange={handleChange}
/>
<TextField
label="Password"
name="password"
value={formData?.snowflake?.password || ''}
required
fullWidth
sx={inputStyle}
onChange={handleChange}
/>
<TextField
label="Database"
name="database"
value={formData?.snowflake?.database || ''}
required
fullWidth
sx={inputStyle}
onChange={handleChange}
/>{' '}
<TextField
label="Application"
name="application"
value={formData?.snowflake?.application || ''}
required
fullWidth
sx={inputStyle}
onChange={handleChange}
/>
<TextField
label="Schema"
name="schema"
value={formData?.snowflake?.schema || ''}
required
fullWidth
sx={inputStyle}
onChange={handleChange}
/>
<TextField
label="Warehouse"
name="warehouse"
value={formData?.snowflake?.warehouse || ''}
required
fullWidth
sx={inputStyle}
onChange={handleChange}
/>
<SubmitButton label="TEST CONNECT" type="submit" sx={{ height: '50px', fontSize: '13px' }} />
</Stack>
</Stack>
);
};
export default SnowflakeDatabaseForm;

View File

@@ -0,0 +1,55 @@
import React from 'react';
import { Stack, TextField } from '@mui/material';
import SubmitButton from '@/components/button/SubmitButton';
const inputStyle = {
width: '800px',
};
const SqliteDatabaseForm = props => {
const { testConnect, formData, setFormData } = props;
const handleSubmit = data => {
data.preventDefault();
const item = {
name: data.target.name.value,
filename: data.target.filename.value,
};
testConnect(item);
};
const handleNameChange = event => {
setFormData(prevState => ({ ...prevState, [event.target.name]: event.target.value }));
};
const handleChange = event => {
setFormData(prevState => ({ ...prevState, sqlite: { ...prevState.sqlite, [event.target.name]: event.target.value } }));
};
return (
<Stack component="form" sx={{ maxWidth: 800, mx: 'auto', mt: 3 }} onSubmit={handleSubmit}>
<Stack sx={{ display: 'flex', justifyContent: 'space-between' }} spacing="20px">
<TextField
label="이름"
name="name"
value={formData?.name || ''}
required
fullWidth
sx={inputStyle}
onChange={handleNameChange}
/>
<TextField
label="Filename"
name="filename"
value={formData?.sqlite?.filename || ''}
required
fullWidth
sx={inputStyle}
onChange={handleChange}
/>
<SubmitButton label="TEST CONNECT" type="submit" sx={{ height: '50px', fontSize: '13px' }} />
</Stack>
</Stack>
);
};
export default SqliteDatabaseForm;

View File

@@ -1,4 +1,4 @@
import React, { useLayoutEffect, useState } from 'react';
import React, { useContext, useEffect, useLayoutEffect, useState } from 'react';
import { Stack, Typography } from '@mui/material';
import PageContainer from '@/components/PageContainer';
import PageTitleBox from '@/components/PageTitleBox';
@@ -10,23 +10,66 @@ import { getDatabaseIcon } from '@/widget/utils/iconUtil';
import { useLocation, useNavigate, useParams } from 'react-router-dom';
import { useAlert } from 'react-alert';
import DatabaseForm from '@/pages/Data/DataSource/form/DatabaseForm';
import SqliteDatabaseForm from '@/pages/Data/DataSource/form/SqliteDatabaseForm';
import BigQueryDatabaseForm from '@/pages/Data/DataSource/form/BigQueryDatabaseForm';
import { SnackbarContext } from '@/contexts/AlertContext';
import { LoadingContext } from '@/contexts/LoadingContext';
import SnowflakeDatabaseForm from '@/pages/Data/DataSource/form/SnowflakeDatabaseForm';
import OracleDatabaseForm from '@/pages/Data/DataSource/form/OracleDatabaseForm';
function DataSource() {
const { sourceId } = useParams();
const { pathname } = useLocation();
const navigate = useNavigate();
const alert = useAlert();
const snackbar = useAlert(SnackbarContext);
const { showLoading, hideLoading } = useContext(LoadingContext);
const [isModifyMode, setIsModifyMode] = useState(false);
const [isConnected, setIsConnected] = useState(false);
const [dataType, setDataType] = useState(null);
const [typeList, setTypeList] = useState([]);
const [formData, setFormData] = useState({
databaseName: '',
host: '',
port: '',
user: '',
password: '',
database: '',
const [formData, setFormData] = useState<any>({
type: '',
name: '',
default: {
host: '',
port: null,
user: '',
password: '',
database: '',
},
sqlite: {
// name: '',
filename: '',
},
bigquery: {
// name: '',
projectId: '',
filename: '',
database: '',
},
oracle: {
// name: '',
host: '',
port: null,
user: '',
password: '',
database: '',
instanceName: '',
fetchAsString: '',
requestTimeout: '',
},
snowflake: {
// name: '',
account: '',
username: '',
password: '',
database: '',
application: '',
schema: '',
warehouse: '',
},
});
useLayoutEffect(() => {
@@ -44,93 +87,180 @@ function DataSource() {
/**
* 수정일 경우 데이터베이스 정보 조회
*/
useEffect(() => {
getType();
}, [typeList, formData.type]);
const getDatabaseInfo = () => {
DatabaseService.selectDatabase(sourceId).then(response => {
const info = response.data;
if (info.status === 'SUCCESS') {
const databaseInfo = info.data.databaseInfo;
const temp = {
databaseName: databaseInfo.name,
host: databaseInfo.connectionConfig.host,
port: databaseInfo.connectionConfig.port,
user: databaseInfo.connectionConfig.user,
password: databaseInfo.connectionConfig.password,
database: databaseInfo.connectionConfig.database,
};
setFormData(temp);
}
});
showLoading();
DatabaseService.selectDatabaseInfo(sourceId)
.then(response => {
const info = response.data;
if (info.status === 'SUCCESS') {
const databaseInfo = info.data.databaseInfo;
const temp: any = {
name: databaseInfo.name,
type: databaseInfo.type,
};
if (databaseInfo.type === 'sqlite') {
temp.sqlite = {
filename: databaseInfo.connectionConfig.filename,
};
} else if (databaseInfo.type === 'bigquery') {
temp.bigquery = {
projectId: databaseInfo.connectionConfig.projectId,
keyFilename: databaseInfo.connectionConfig.keyFilename,
schema: databaseInfo.connectionConfig.schema,
};
} else if (databaseInfo.type === 'oracle') {
temp.oracle = {
host: databaseInfo.connectionConfig.host,
port: Number(databaseInfo.connectionConfig.port),
user: databaseInfo.connectionConfig.user,
password: databaseInfo.connectionConfig.password,
database: databaseInfo.connectionConfig.database,
instanceName: databaseInfo.connectionConfig.instanceName,
fetchAsString: databaseInfo.connectionConfig.fetchAsString,
requestTimeout: databaseInfo.connectionConfig.requestTimeout,
};
} else if (databaseInfo.type === 'snowflake') {
temp.snowflake = {
account: databaseInfo.connectionConfig.account,
username: databaseInfo.connectionConfig.username,
password: databaseInfo.connectionConfig.password,
database: databaseInfo.connectionConfig.database,
application: databaseInfo.connectionConfig.application,
schema: databaseInfo.connectionConfig.schema,
warehouse: databaseInfo.connectionConfig.warehouse,
};
} else {
temp.default = {
host: databaseInfo.connectionConfig.host,
port: Number(databaseInfo.connectionConfig.port),
user: databaseInfo.connectionConfig.user,
password: databaseInfo.connectionConfig.password,
database: databaseInfo.connectionConfig.database,
};
}
setFormData(temp);
}
})
.finally(() => {
hideLoading();
});
};
const testConnect = item => {
showLoading();
const param = {
// name: item.name,
// description: item.name,
name: item.name,
description: item.name,
connectionConfig: item,
engine: dataType.type,
engine: dataType.engine,
};
console.log(param, 'param');
DatabaseService.testConnection(param).then(response => {
console.log(response);
if (response.data.status === STATUS.SUCCESS) {
setIsConnected(true);
alert.info('데이터베이스 연결에 성공하였습니다.');
} else {
alert.info('데이터베이스에 연결할 수 없습니다.\n데이터 베이스 정보를 확인해주세요.');
}
});
DatabaseService.testConnection(param)
.then(response => {
console.log(response);
if (response.data.status === STATUS.SUCCESS) {
setIsConnected(true);
snackbar.success('데이터베이스 연결에 성공습니다.');
} else {
snackbar.error('데이터베이스에 연결할 수 없습니다. 데이터베이스 정보를 확인해주세요');
}
})
.finally(() => {
hideLoading();
});
};
const getDatabaseTypeList = () => {
DatabaseService.selectDatabaseTypeList().then(response => {
console.log('selectDatabaseTypeList', response.data);
if (response.data.status === STATUS.SUCCESS) {
const list = response.data.data;
list.map(item => (item.icon = getDatabaseIcon(item.type)));
setTypeList(list);
if (list.length > 0) {
setDataType(list[0]);
showLoading();
DatabaseService.selectDatabaseTypeList()
.then(response => {
console.log('selectDatabaseTypeList', response.data);
if (response.data.status === STATUS.SUCCESS) {
const list = response.data.data;
list.map(item => (item.icon = getDatabaseIcon(item.type)));
setTypeList(list);
}
}
});
})
.finally(() => {
hideLoading();
});
};
const getType = () => {
if (typeList.length > 0 && formData.type) {
const type = typeList.filter(item => item.type === formData.type);
setDataType(type[0]);
}
};
const getFormComponentType = () => {
switch (dataType.type) {
case 'sqlite':
return 'sqlite';
case 'bigquery':
return 'bigquery';
case 'oracle':
return 'oracle';
case 'snowflake':
return 'snowflake';
default:
return 'default';
}
};
const handleSaveClick = () => {
const param = {
name: formData.databaseName,
description: formData.databaseName,
connectionConfig: formData,
engine: dataType.type,
name: formData.name,
description: formData.name,
type: dataType.type,
engine: dataType.engine,
connectionConfig: formData[getFormComponentType()],
};
// 숫자 형변환
if (param?.connectionConfig?.port) {
param.connectionConfig.port = Number(param.connectionConfig.port);
}
alert.success(`데이터베이스를 ${isModifyMode ? '수정' : '생성'}하시겠습니까?`, {
closeCopy: '취소',
actions: [
{
copy: '확인',
onClick: () => {
showLoading();
if (isModifyMode) {
DatabaseService.updateDatabase(sourceId, param).then(response => {
console.log(response.data);
if (response.data.status === STATUS.SUCCESS) {
console.log('데이터 베이스 저장', param);
alert.info('데이터베이스가 수정되었습니다.', {
onClose: () => {
navigate('/data');
},
});
}
});
DatabaseService.updateDatabase(sourceId, param)
.then(response => {
console.log(response.data);
if (response.data.status === STATUS.SUCCESS) {
console.log('데이터베이스 저장', param);
navigate('/data');
snackbar.success('데이터베이스가 수정되었습니다.');
} else {
alert.error('데이터베이스 저장에 실패했습니다.');
}
})
.finally(() => {
hideLoading();
});
} else {
DatabaseService.createDatabase(param).then(response => {
if (response.data.status === STATUS.SUCCESS) {
alert.info('데이터베이스가 생성되었습니다.', {
onClose: () => {
navigate('/data');
},
});
}
});
DatabaseService.createDatabase(param)
.then(response => {
if (response.data.status === STATUS.SUCCESS) {
console.log('데이터베이스 저장', param);
navigate('/data');
snackbar.success('데이터베이스가 생성되었습니다.');
} else {
alert.error('데이터베이스 저장에 실패했습니다.');
}
})
.finally(() => {
hideLoading();
});
}
},
},
@@ -138,10 +268,32 @@ function DataSource() {
});
};
const handleTypeClick = item => {
setDataType(item);
setFormData({ type: item.type });
};
const handleCancelClick = () => {
navigate('/data');
};
const dbType = () => {
switch (dataType?.type) {
case 'sqlite':
return <SqliteDatabaseForm testConnect={testConnect} formData={formData} setFormData={setFormData} />;
case 'bigquery':
return <BigQueryDatabaseForm testConnect={testConnect} formData={formData} setFormData={setFormData} />;
case 'oracle':
return <OracleDatabaseForm testConnect={testConnect} formData={formData} setFormData={setFormData} />;
case 'snowflake':
return <SnowflakeDatabaseForm testConnect={testConnect} formData={formData} setFormData={setFormData} />;
case undefined:
return <Typography sx={{ my: '160px', mx: 'auto' }}> .</Typography>;
default:
return <DatabaseForm testConnect={testConnect} formData={formData} setFormData={setFormData} />;
}
};
return (
<PageContainer>
<PageTitleBox
@@ -150,7 +302,7 @@ function DataSource() {
title={'데이터 소스 연결'}
button={
<ConfirmCancelButton
confirmProps={{ disabled: false, onClick: handleSaveClick }}
confirmProps={{ disabled: !isConnected, onClick: handleSaveClick }}
cancelProps={{ onClick: handleCancelClick }}
/>
}
@@ -165,7 +317,12 @@ function DataSource() {
>
step.01
</Typography>
<ImgCardList data={typeList} selectedType={dataType} setSelectedType={setDataType} />
<ImgCardList
data={typeList}
selectedType={dataType}
setSelectedType={setDataType}
handleTypeClick={handleTypeClick}
/>
</Stack>
<Stack sx={{ p: '30px 25px 50px 25px', bgcolor: '#f5f6f8' }}>
<Typography
@@ -175,7 +332,7 @@ function DataSource() {
>
step.02
</Typography>
<DatabaseForm testConnect={testConnect} formData={formData} setFormData={setFormData} />
{dbType()}
</Stack>
</Stack>
</PageTitleBox>

View File

@@ -7,7 +7,7 @@ import PageTitleBox from '@/components/PageTitleBox';
* 데이터 관리 페이지
* @constructor
*/
function DataPage() {
const Data = () => {
return (
<PageContainer>
<PageTitleBox title="데이터" sx={{ paddingLeft: 0, paddingRight: 0, width: '100%', height: '100%' }}>
@@ -15,6 +15,6 @@ function DataPage() {
</PageTitleBox>
</PageContainer>
);
}
};
export default DataPage;
export default Data;

View File

@@ -0,0 +1,120 @@
import React, { useContext, useState } from 'react';
import { useNavigate } from 'react-router-dom';
import { Box, Button, Link, Stack, TextField, Typography } from '@mui/material';
import { useAlert } from 'react-alert';
import { useAuth } from '@/contexts/AuthContext';
import { LoadingContext } from '@/contexts/LoadingContext';
import { ReactComponent as Logo } from '@/assets/images/logo.svg';
import backgroundImage from '@/assets/images/visual-bg.png';
function Copyright(props: any) {
return (
<Typography color="text.secondary" align="center" {...props}>
<Link
color="inherit"
href="https://vanillabrain.com/"
sx={{ fontSize: '13px', color: '#767676', textDecoration: 'none' }}
>
@ Vanilla Meta 2022
</Link>
{/*{' '}*/}
{/*{new Date().getFullYear()}*/}
{/*{'.'}*/}
</Typography>
);
}
const Login = () => {
const { onLogin } = useAuth();
const navigate = useNavigate();
const alert = useAlert();
const { showLoading, hideLoading } = useContext(LoadingContext);
const [userInfo] = useState({
userId: process.env.REACT_APP_MODE === 'local' ? process.env.REACT_APP_ID : '',
userPwd: process.env.REACT_APP_MODE === 'local' ? process.env.REACT_APP_PWD : '',
});
const handleLogin = async event => {
event.preventDefault();
showLoading();
await onLogin(event.target.userId.value, event.target.userPwd.value)
.then(res => {
if (res) {
navigate('/dashboard');
}
})
.catch(error => {
alert.error(error.message);
})
.finally(() => {
hideLoading();
});
};
return (
<Box
component="main"
sx={{ position: 'relative', zIndex: 0, width: '100%', height: '100%', minWidth: '100%', backgroundColor: '#f5f6f8' }}
>
<Box
sx={{
pt: '90px',
display: 'flex',
flexDirection: 'column',
alignItems: 'center',
}}
>
<Logo width="223px" height="43px" />
<Typography sx={{ mt: '17px', fontSize: '16px', color: '#043f84' }}>
{' '}
<Typography component="span" sx={{ fontSize: '16px', fontWeight: 'bold' }}>
</Typography>
</Typography>
<Stack component="form" onSubmit={handleLogin} noValidate sx={{ width: '360px', mt: '56px' }} spacing="20px">
<TextField
margin="normal"
required
fullWidth
id="userId"
label="User ID"
name="email"
defaultValue={userInfo.userId}
/>
<TextField
margin="normal"
required
fullWidth
name="userPwd"
defaultValue={userInfo.userPwd}
label="Password"
type="password"
id="password"
sx={{ height: '36px' }}
/>
{/*<FormControlLabel control={<Checkbox value="remember" color="primary" />} label="Remember me" />*/}
<Button type="submit" size="large" fullWidth variant="contained" sx={{ mt: 3, mb: 2 }}>
Login
</Button>
</Stack>
</Box>
<Copyright sx={{ mt: 8, mb: 4 }} />
<Box
component="img"
src={backgroundImage}
sx={{
position: 'fixed',
zIndex: -1,
bottom: 0,
left: 0,
right: 0,
margin: 'auto',
width: '1024px',
height: '608px',
}}
/>
</Box>
);
};
export default Login;

View File

@@ -8,13 +8,13 @@ import { STATUS } from '@/constant';
import { LayoutContext } from '@/contexts/LayoutContext';
import { LoadingContext } from '@/contexts/LoadingContext';
import grid from '@/assets/images/grid.svg';
import { useNavigate } from 'react-router-dom';
import { SnackbarContext } from '@/contexts/AlertContext';
const WidgetAttributeSelect = props => {
const { widgetOption, saveWidgetInfo, dataset, isModifyMode = false, widgetTypeName, widgetTypeDescription } = props;
const alert = useAlert();
const navigate = useNavigate();
const snackbar = useAlert(SnackbarContext);
const { fixLayout } = useContext(LayoutContext);
const { showLoading, hideLoading } = useContext(LoadingContext);
@@ -46,7 +46,9 @@ const WidgetAttributeSelect = props => {
const getData = () => {
const param = isModifyMode
? { datasetType: widgetOption.datasetType, datasetId: widgetOption.datasetId }
: { datasetType: dataset.datasetType, datasetId: dataset.id };
: dataset.datasetType === 'TABLE'
? { databaseId: dataset.databaseId, datasetType: dataset.datasetType, tableName: dataset.tableName }
: { databaseId: dataset.databaseId, datasetType: dataset.datasetType, datasetId: dataset.id };
console.log('getData param', param);
showLoading();
DatabaseService.selectData(param)
@@ -65,7 +67,7 @@ const WidgetAttributeSelect = props => {
event.preventDefault();
// confirm sample
alert.success('위젯 속성을 저장하시겠습니까?', {
alert.success('위젯을 저장하시겠습니까?', {
title: '위젯 저장',
closeCopy: '취소',
actions: [
@@ -73,7 +75,7 @@ const WidgetAttributeSelect = props => {
copy: '저장',
onClick: () => {
saveWidgetInfo(option, title);
navigate('/widget');
snackbar.success('위젯이 저장되었습니다.');
},
},
],
@@ -99,12 +101,14 @@ const WidgetAttributeSelect = props => {
<Box
sx={{
width: '60%',
height: '500px',
minHeight: '500px',
maxHeight: '100%',
margin: '54px auto',
border: '1px solid #e2e2e2',
borderRadius: '8px',
boxShadow: '2px 2px 9px 0 rgba(42, 50, 62, 0.1), 0 4px 4px 0 rgba(0, 0, 0, 0.02)',
backgroundColor: '#fff',
overflow: 'hidden',
}}
>
<WidgetViewer

View File

@@ -1,39 +1,9 @@
import React, { useEffect, useState } from 'react';
import React from 'react';
import DataLayout from '@/pages/Data/DataLayout';
function WidgetDataSelect(props) {
const { setDataSet } = props;
const [isLoading, setIsLoading] = useState(false);
const [loadedData, setLoadedData] = useState([]);
useEffect(() => {
setIsLoading(true);
}, []);
// 선택한 데이터베이스를 CardList에서 가져와서 저장하고 dataSet과 dataList를 보여주는 state
const [presentedData, setPresentedData] = useState({
dataSource: 0,
dataList: '',
dataSet: '',
});
const handleUpdate = enteredData => {
if (typeof enteredData === 'object') {
setPresentedData(prevState => ({ ...prevState, ...enteredData }));
}
};
useEffect(() => {
loadedData.filter((item, index) => {
if (presentedData.dataSource === item.id) {
const selectedArray = loadedData[index];
handleUpdate({ dataList: selectedArray.dataList, dataSet: selectedArray.dataSet });
// console.log(presentedData);
}
});
}, [presentedData.dataSource]);
return <DataLayout isViewMode={true} setDataSet={setDataSet} />;
}

Some files were not shown because too many files have changed in this diff Show More