sql-assessment extension code (#12948)
sql-assessment extension on model view components base
@@ -32,15 +32,15 @@
|
|||||||
"title": "%displayName%",
|
"title": "%displayName%",
|
||||||
"when": "connectionProvider == 'MSSQL' && !mssql:iscloud && mssql:engineedition != 11",
|
"when": "connectionProvider == 'MSSQL' && !mssql:iscloud && mssql:engineedition != 11",
|
||||||
"container": {
|
"container": {
|
||||||
"controlhost-container": {
|
"modelview-container": null
|
||||||
"type": "assessment"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"vscode-nls": "^3.2.1"
|
"vscode-nls": "^3.2.1",
|
||||||
|
"ads-extension-telemetry": "github:Charles-Gagnon/ads-extension-telemetry#0.1.0",
|
||||||
|
"vscode-languageclient": "^5.3.0-next.1"
|
||||||
},
|
},
|
||||||
"__metadata": {
|
"__metadata": {
|
||||||
"id": "67",
|
"id": "67",
|
||||||
|
|||||||
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M15 8H8V15H0V0H15V8ZM7 8H1V14H7V8ZM7 1H1V7H7V1ZM14 1H8V7H14V1ZM14.9531 12.0234C14.9844 12.1797 15 12.3385 15 12.5C15 12.6615 14.9844 12.8203 14.9531 12.9766L15.9219 13.375L15.5391 14.3047L14.5703 13.8984C14.4818 14.0339 14.3802 14.1589 14.2656 14.2734C14.1562 14.3828 14.0339 14.4818 13.8984 14.5703L14.3047 15.5391L13.375 15.9219L12.9766 14.9531C12.8203 14.9844 12.6615 15 12.5 15C12.3385 15 12.1797 14.9844 12.0234 14.9531L11.625 15.9219L10.6953 15.5391L11.1016 14.5703C10.8307 14.3932 10.6068 14.1693 10.4297 13.8984L9.46094 14.3047L9.07812 13.375L10.0469 12.9766C10.0156 12.8203 10 12.6615 10 12.5C10 12.3385 10.0156 12.1797 10.0469 12.0234L9.07812 11.625L9.46094 10.6953L10.4297 11.1016C10.5182 10.9661 10.6172 10.8438 10.7266 10.7344C10.8411 10.6198 10.9661 10.5182 11.1016 10.4297L10.6953 9.46094L11.625 9.07812L12.0234 10.0469C12.0964 10.0365 12.1667 10.0286 12.2344 10.0234C12.3073 10.013 12.3802 10.0078 12.4531 10.0078C12.5417 10.0078 12.6276 10.013 12.7109 10.0234C12.7943 10.0339 12.8802 10.0469 12.9688 10.0625L13.375 9.07812L14.3047 9.46094L13.8906 10.4453C14.026 10.5339 14.151 10.6328 14.2656 10.7422C14.3802 10.8464 14.4818 10.9661 14.5703 11.1016L15.5391 10.6953L15.9219 11.625L14.9531 12.0234ZM12.5078 14C12.7109 14 12.9036 13.9609 13.0859 13.8828C13.2682 13.7995 13.4271 13.6901 13.5625 13.5547C13.6979 13.4193 13.8047 13.2604 13.8828 13.0781C13.9609 12.8958 14 12.7031 14 12.5C14 12.2917 13.9609 12.0964 13.8828 11.9141C13.8047 11.7318 13.6979 11.5729 13.5625 11.4375C13.4271 11.3021 13.2682 11.1953 13.0859 11.1172C12.9036 11.0391 12.7083 11 12.5 11C12.2969 11 12.1042 11.0391 11.9219 11.1172C11.7396 11.1953 11.5781 11.3047 11.4375 11.4453C11.3021 11.5807 11.1953 11.7396 11.1172 11.9219C11.0391 12.1042 11 12.2969 11 12.5C11 12.7083 11.0391 12.9036 11.1172 13.0859C11.2005 13.2682 11.3099 13.4271 11.4453 13.5625C11.5807 13.6979 11.7396 13.8047 11.9219 13.8828C12.1094 13.9609 12.3047 14 12.5078 14Z" fill="#ffffff"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
1
extensions/sql-assessment/resources/dark/database.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#f6f6f6;}.cls-2{fill:#424242;}.cls-3{fill:#f0eff1;}</style></defs><title>database_16x</title><g id="outline"><path class="cls-1" d="M8,0C4,0,1,1.72,1,4v8c0,2.28,3,4,7,4s7-1.72,7-4V4C15,1.72,12,0,8,0Z"/></g><g id="icon_bg"><path class="cls-2" d="M8,1C4.69,1,2,2.34,2,4v8c0,1.66,2.69,3,6,3s6-1.34,6-3V4C14,2.34,11.31,1,8,1ZM8,5.88C5.07,5.88,3.12,4.75,3.12,4S5.07,2.13,8,2.13,12.87,3.25,12.87,4,10.93,5.88,8,5.88Z"/></g><g id="icon_fg"><path class="cls-3" d="M8,2.13C5.07,2.13,3.12,3.25,3.12,4S5.07,5.88,8,5.88,12.87,4.75,12.87,4,10.93,2.13,8,2.13Z"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 666 B |
43
extensions/sql-assessment/resources/dark/history.svg
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||||
|
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||||
|
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="980.000000pt" height="980.000000pt" viewBox="0 0 980.000000 980.000000"
|
||||||
|
preserveAspectRatio="xMidYMid meet">
|
||||||
|
<g transform="translate(0.000000,980.000000) scale(0.100000,-0.100000)"
|
||||||
|
fill="#ffffff" stroke="none">
|
||||||
|
<path d="M416 9784 c-141 -34 -282 -144 -350 -273 -72 -137 -66 279 -66 -4611
|
||||||
|
0 -4892 -6 -4474 66 -4611 71 -135 228 -250 378 -278 43 -8 1098 -11 3646 -11
|
||||||
|
3541 0 3588 0 3664 20 43 11 101 32 129 47 73 39 178 146 215 219 56 110 65
|
||||||
|
161 66 354 l1 175 -135 0 -135 0 -5 -175 c-4 -146 -8 -181 -24 -214 -27 -53
|
||||||
|
-80 -104 -135 -130 l-46 -21 -3605 0 -3605 0 -41 22 c-64 34 -103 71 -132 127
|
||||||
|
l-27 51 0 4425 0 4425 23 47 c26 53 87 112 141 136 33 16 225 17 2384 20
|
||||||
|
l2347 2 0 -567 c0 -448 3 -582 14 -634 42 -195 184 -349 386 -415 52 -17 125
|
||||||
|
-19 1188 -22 l1132 -3 0 -269 0 -270 141 0 141 0 -5 293 c-5 324 -10 357 -73
|
||||||
|
478 -70 134 -49 120 -1347 898 -653 392 -1211 723 -1240 735 -28 13 -76 28
|
||||||
|
-105 35 -39 8 -713 11 -2490 10 -2067 -1 -2446 -3 -2496 -15z"/>
|
||||||
|
<path d="M1090 8165 l0 -275 1768 2 1767 3 3 273 2 272 -1770 0 -1770 0 0
|
||||||
|
-275z"/>
|
||||||
|
<path d="M6260 7340 c-349 -34 -667 -112 -986 -243 -268 -110 -536 -269 -775
|
||||||
|
-458 -122 -97 -339 -307 -449 -434 -361 -419 -618 -952 -724 -1504 -46 -238
|
||||||
|
-51 -301 -51 -626 1 -326 12 -445 66 -690 133 -608 440 -1167 884 -1610 506
|
||||||
|
-506 1117 -813 1845 -927 128 -20 181 -22 465 -22 283 0 337 2 465 22 619 97
|
||||||
|
1147 333 1625 727 128 106 358 341 464 474 480 605 731 1360 708 2126 -17 554
|
||||||
|
-165 1075 -439 1549 -63 109 -200 308 -290 420 -102 127 -353 378 -478 477
|
||||||
|
-491 392 -1026 620 -1650 704 -138 19 -546 27 -680 15z m610 -556 c1255 -157
|
||||||
|
2234 -1157 2370 -2420 16 -148 8 -543 -14 -684 -90 -575 -354 -1096 -761
|
||||||
|
-1506 -77 -78 -183 -175 -235 -216 -687 -543 -1554 -724 -2399 -502 -182 47
|
||||||
|
-296 91 -496 189 -819 400 -1375 1172 -1502 2086 -24 173 -24 539 1 704 73
|
||||||
|
499 246 917 543 1308 112 147 372 404 523 516 448 333 932 508 1510 545 93 6
|
||||||
|
339 -5 460 -20z"/>
|
||||||
|
<path d="M6447 6246 c-92 -33 -157 -106 -177 -199 -8 -37 -10 -344 -8 -1047
|
||||||
|
l3 -995 22 -41 c27 -50 76 -99 128 -127 l40 -22 869 -3 c1000 -3 942 -7 1032
|
||||||
|
82 41 42 56 66 70 111 38 134 -24 268 -151 326 -37 17 -85 19 -752 19 l-712 0
|
||||||
|
-3 848 -3 847 -24 53 c-57 126 -208 193 -334 148z"/>
|
||||||
|
<path d="M1090 5990 l0 -270 1090 0 1090 0 0 270 0 270 -1090 0 -1090 0 0
|
||||||
|
-270z"/>
|
||||||
|
<path d="M1090 3810 l0 -270 815 0 815 0 0 270 0 270 -815 0 -815 0 0 -270z"/>
|
||||||
|
<path d="M1090 1635 l0 -275 1225 0 1225 0 0 275 0 275 -1225 0 -1225 0 0
|
||||||
|
-275z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.5 KiB |
@@ -0,0 +1 @@
|
|||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1,.cls-2{fill:#fff;}.cls-2{font-size:12px;font-family:FullMDL2Assets, Full MDL2 Assets;}</style></defs><title>newquery_inverse_16x16</title><path class="cls-1" d="M1.79,3.85H.26V2.33H1.79Zm0,9.14H.26V11.47H1.79ZM15.51,3.85H4.08V2.33H15.51Zm0,9.14H4.08V11.47H15.51Zm0-6.09H7.12V5.38h8.39Zm0,3H10.18V8.42h5.34Z"/><text class="cls-2" transform="translate(0.01 11.59)"> </text></svg>
|
||||||
|
After Width: | Height: | Size: 490 B |
1
extensions/sql-assessment/resources/dark/server.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#ffffff" d="m 2.735,0 v 16 h 10.53 V 0 Z m 1,1 h 8.53 v 9 h -8.53 z m 8.53,14 h -8.53 v -4 h 8.53 z"/><path fill="#ffffff" d="M 7.125,4.23 H 4.675 V 1.77 h 2.45 z m -2,-0.5 h 1.5 V 2.27 h -1.45 z"/></svg>
|
||||||
|
After Width: | Height: | Size: 277 B |
@@ -0,0 +1,3 @@
|
|||||||
|
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M15 8H8V15H0V0H15V8ZM7 8H1V14H7V8ZM7 1H1V7H7V1ZM14 1H8V7H14V1ZM14.9531 12.0234C14.9844 12.1797 15 12.3385 15 12.5C15 12.6615 14.9844 12.8203 14.9531 12.9766L15.9219 13.375L15.5391 14.3047L14.5703 13.8984C14.4818 14.0339 14.3802 14.1589 14.2656 14.2734C14.1562 14.3828 14.0339 14.4818 13.8984 14.5703L14.3047 15.5391L13.375 15.9219L12.9766 14.9531C12.8203 14.9844 12.6615 15 12.5 15C12.3385 15 12.1797 14.9844 12.0234 14.9531L11.625 15.9219L10.6953 15.5391L11.1016 14.5703C10.8307 14.3932 10.6068 14.1693 10.4297 13.8984L9.46094 14.3047L9.07812 13.375L10.0469 12.9766C10.0156 12.8203 10 12.6615 10 12.5C10 12.3385 10.0156 12.1797 10.0469 12.0234L9.07812 11.625L9.46094 10.6953L10.4297 11.1016C10.5182 10.9661 10.6172 10.8438 10.7266 10.7344C10.8411 10.6198 10.9661 10.5182 11.1016 10.4297L10.6953 9.46094L11.625 9.07812L12.0234 10.0469C12.0964 10.0365 12.1667 10.0286 12.2344 10.0234C12.3073 10.013 12.3802 10.0078 12.4531 10.0078C12.5417 10.0078 12.6276 10.013 12.7109 10.0234C12.7943 10.0339 12.8802 10.0469 12.9688 10.0625L13.375 9.07812L14.3047 9.46094L13.8906 10.4453C14.026 10.5339 14.151 10.6328 14.2656 10.7422C14.3802 10.8464 14.4818 10.9661 14.5703 11.1016L15.5391 10.6953L15.9219 11.625L14.9531 12.0234ZM12.5078 14C12.7109 14 12.9036 13.9609 13.0859 13.8828C13.2682 13.7995 13.4271 13.6901 13.5625 13.5547C13.6979 13.4193 13.8047 13.2604 13.8828 13.0781C13.9609 12.8958 14 12.7031 14 12.5C14 12.2917 13.9609 12.0964 13.8828 11.9141C13.8047 11.7318 13.6979 11.5729 13.5625 11.4375C13.4271 11.3021 13.2682 11.1953 13.0859 11.1172C12.9036 11.0391 12.7083 11 12.5 11C12.2969 11 12.1042 11.0391 11.9219 11.1172C11.7396 11.1953 11.5781 11.3047 11.4375 11.4453C11.3021 11.5807 11.1953 11.7396 11.1172 11.9219C11.0391 12.1042 11 12.2969 11 12.5C11 12.7083 11.0391 12.9036 11.1172 13.0859C11.2005 13.2682 11.3099 13.4271 11.4453 13.5625C11.5807 13.6979 11.7396 13.8047 11.9219 13.8828C12.1094 13.9609 12.3047 14 12.5078 14Z" fill="#333333"/>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
1
extensions/sql-assessment/resources/light/database.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#f6f6f6;}.cls-2{fill:#424242;}.cls-3{fill:#f0eff1;}</style></defs><title>database_16x</title><g id="outline"><path class="cls-1" d="M8,0C4,0,1,1.72,1,4v8c0,2.28,3,4,7,4s7-1.72,7-4V4C15,1.72,12,0,8,0Z"/></g><g id="icon_bg"><path class="cls-2" d="M8,1C4.69,1,2,2.34,2,4v8c0,1.66,2.69,3,6,3s6-1.34,6-3V4C14,2.34,11.31,1,8,1ZM8,5.88C5.07,5.88,3.12,4.75,3.12,4S5.07,2.13,8,2.13,12.87,3.25,12.87,4,10.93,5.88,8,5.88Z"/></g><g id="icon_fg"><path class="cls-3" d="M8,2.13C5.07,2.13,3.12,3.25,3.12,4S5.07,5.88,8,5.88,12.87,4.75,12.87,4,10.93,2.13,8,2.13Z"/></g></svg>
|
||||||
|
After Width: | Height: | Size: 666 B |
43
extensions/sql-assessment/resources/light/history.svg
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
<?xml version="1.0" standalone="no"?>
|
||||||
|
<!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 20010904//EN"
|
||||||
|
"http://www.w3.org/TR/2001/REC-SVG-20010904/DTD/svg10.dtd">
|
||||||
|
<svg version="1.0" xmlns="http://www.w3.org/2000/svg"
|
||||||
|
width="980.000000pt" height="980.000000pt" viewBox="0 0 980.000000 980.000000"
|
||||||
|
preserveAspectRatio="xMidYMid meet">
|
||||||
|
<g transform="translate(0.000000,980.000000) scale(0.100000,-0.100000)"
|
||||||
|
fill="#000000" stroke="none">
|
||||||
|
<path d="M416 9784 c-141 -34 -282 -144 -350 -273 -72 -137 -66 279 -66 -4611
|
||||||
|
0 -4892 -6 -4474 66 -4611 71 -135 228 -250 378 -278 43 -8 1098 -11 3646 -11
|
||||||
|
3541 0 3588 0 3664 20 43 11 101 32 129 47 73 39 178 146 215 219 56 110 65
|
||||||
|
161 66 354 l1 175 -135 0 -135 0 -5 -175 c-4 -146 -8 -181 -24 -214 -27 -53
|
||||||
|
-80 -104 -135 -130 l-46 -21 -3605 0 -3605 0 -41 22 c-64 34 -103 71 -132 127
|
||||||
|
l-27 51 0 4425 0 4425 23 47 c26 53 87 112 141 136 33 16 225 17 2384 20
|
||||||
|
l2347 2 0 -567 c0 -448 3 -582 14 -634 42 -195 184 -349 386 -415 52 -17 125
|
||||||
|
-19 1188 -22 l1132 -3 0 -269 0 -270 141 0 141 0 -5 293 c-5 324 -10 357 -73
|
||||||
|
478 -70 134 -49 120 -1347 898 -653 392 -1211 723 -1240 735 -28 13 -76 28
|
||||||
|
-105 35 -39 8 -713 11 -2490 10 -2067 -1 -2446 -3 -2496 -15z"/>
|
||||||
|
<path d="M1090 8165 l0 -275 1768 2 1767 3 3 273 2 272 -1770 0 -1770 0 0
|
||||||
|
-275z"/>
|
||||||
|
<path d="M6260 7340 c-349 -34 -667 -112 -986 -243 -268 -110 -536 -269 -775
|
||||||
|
-458 -122 -97 -339 -307 -449 -434 -361 -419 -618 -952 -724 -1504 -46 -238
|
||||||
|
-51 -301 -51 -626 1 -326 12 -445 66 -690 133 -608 440 -1167 884 -1610 506
|
||||||
|
-506 1117 -813 1845 -927 128 -20 181 -22 465 -22 283 0 337 2 465 22 619 97
|
||||||
|
1147 333 1625 727 128 106 358 341 464 474 480 605 731 1360 708 2126 -17 554
|
||||||
|
-165 1075 -439 1549 -63 109 -200 308 -290 420 -102 127 -353 378 -478 477
|
||||||
|
-491 392 -1026 620 -1650 704 -138 19 -546 27 -680 15z m610 -556 c1255 -157
|
||||||
|
2234 -1157 2370 -2420 16 -148 8 -543 -14 -684 -90 -575 -354 -1096 -761
|
||||||
|
-1506 -77 -78 -183 -175 -235 -216 -687 -543 -1554 -724 -2399 -502 -182 47
|
||||||
|
-296 91 -496 189 -819 400 -1375 1172 -1502 2086 -24 173 -24 539 1 704 73
|
||||||
|
499 246 917 543 1308 112 147 372 404 523 516 448 333 932 508 1510 545 93 6
|
||||||
|
339 -5 460 -20z"/>
|
||||||
|
<path d="M6447 6246 c-92 -33 -157 -106 -177 -199 -8 -37 -10 -344 -8 -1047
|
||||||
|
l3 -995 22 -41 c27 -50 76 -99 128 -127 l40 -22 869 -3 c1000 -3 942 -7 1032
|
||||||
|
82 41 42 56 66 70 111 38 134 -24 268 -151 326 -37 17 -85 19 -752 19 l-712 0
|
||||||
|
-3 848 -3 847 -24 53 c-57 126 -208 193 -334 148z"/>
|
||||||
|
<path d="M1090 5990 l0 -270 1090 0 1090 0 0 270 0 270 -1090 0 -1090 0 0
|
||||||
|
-270z"/>
|
||||||
|
<path d="M1090 3810 l0 -270 815 0 815 0 0 270 0 270 -815 0 -815 0 0 -270z"/>
|
||||||
|
<path d="M1090 1635 l0 -275 1225 0 1225 0 0 275 0 275 -1225 0 -1225 0 0
|
||||||
|
-275z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.5 KiB |
1
extensions/sql-assessment/resources/light/newquery.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg id="Layer_1" data-name="Layer 1" xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><defs><style>.cls-1{fill:#212121;}</style></defs><title>newquery_16x16</title><path class="cls-1" d="M1.9,4H.38V2.45H1.9Zm0,9.14H.38V11.59H1.9ZM15.63,4H4.19V2.45H15.63Zm0,9.14H4.19V11.59H15.63Zm0-6.09H7.23V5.5h8.39Zm0,3H10.29V8.54h5.34Z"/></svg>
|
||||||
|
After Width: | Height: | Size: 338 B |
1
extensions/sql-assessment/resources/light/server.svg
Normal file
@@ -0,0 +1 @@
|
|||||||
|
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="#212121" d="m 2.735,0 v 16 h 10.53 V 0 Z m 1,1 h 8.53 v 9 h -8.53 z m 8.53,14 h -8.53 v -4 h 8.53 z"/><path fill="#231f20" d="M 7.125,4.23 H 4.675 V 1.77 h 2.45 z m -2,-0.5 h 1.5 V 2.27 h -1.45 z"/></svg>
|
||||||
|
After Width: | Height: | Size: 277 B |
263
extensions/sql-assessment/src/assessmentResultGrid.ts
Normal file
@@ -0,0 +1,263 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
import * as azdata from 'azdata';
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
import * as nls from 'vscode-nls';
|
||||||
|
|
||||||
|
import { AssessmentType } from './engine';
|
||||||
|
import { LocalizedStrings } from './localized';
|
||||||
|
const localize = nls.loadMessageBundle();
|
||||||
|
|
||||||
|
export class AssessmentResultGrid implements vscode.Disposable {
|
||||||
|
|
||||||
|
private table!: azdata.TableComponent;
|
||||||
|
private rootContainer!: azdata.FlexContainer;
|
||||||
|
private toDispose: vscode.Disposable[] = [];
|
||||||
|
private detailsPanel!: azdata.FlexContainer;
|
||||||
|
private dataItems!: azdata.SqlAssessmentResultItem[];
|
||||||
|
|
||||||
|
private tagsPlaceholder!: azdata.TextComponent;
|
||||||
|
private checkNamePlaceholder!: azdata.TextComponent;
|
||||||
|
private checkDescriptionPlaceholder!: azdata.TextComponent;
|
||||||
|
private clickHereLabel!: azdata.HyperlinkComponent;
|
||||||
|
private asmtMessagePlaceholder!: azdata.TextComponent;
|
||||||
|
private asmtMessageDiv!: azdata.DivContainer;
|
||||||
|
private descriptionCaption!: azdata.TextComponent;
|
||||||
|
|
||||||
|
private asmtType!: AssessmentType;
|
||||||
|
|
||||||
|
private readonly checkIdColOrder = 4;
|
||||||
|
private readonly targetColOrder = 0;
|
||||||
|
|
||||||
|
public get component(): azdata.Component {
|
||||||
|
return this.rootContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
public constructor(view: azdata.ModelView) {
|
||||||
|
const headerCssClass = 'no-borders align-with-header';
|
||||||
|
this.table = view.modelBuilder.table()
|
||||||
|
.withProperties<azdata.TableComponentProperties>({
|
||||||
|
data: [],
|
||||||
|
columns: [
|
||||||
|
{ value: LocalizedStrings.TARGET_COLUMN_NAME, headerCssClass: headerCssClass, width: 125 },
|
||||||
|
{ value: LocalizedStrings.SEVERITY_COLUMN_NAME, headerCssClass: headerCssClass, width: 100 },
|
||||||
|
{ value: LocalizedStrings.MESSAGE_COLUMN_NAME, headerCssClass: headerCssClass, width: 900 },
|
||||||
|
{ value: LocalizedStrings.TAGS_COLUMN_NAME, headerCssClass: headerCssClass, width: 200 },
|
||||||
|
{ value: LocalizedStrings.CHECKID_COLUMN_NAME, headerCssClass: headerCssClass, width: 80 }
|
||||||
|
],
|
||||||
|
width: '100%',
|
||||||
|
height: '100px',
|
||||||
|
headerFilter: true
|
||||||
|
}).component();
|
||||||
|
|
||||||
|
|
||||||
|
this.toDispose.push(
|
||||||
|
this.table.onRowSelected(async () => {
|
||||||
|
if (this.table.selectedRows?.length !== 1) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
await this.showDetails(this.table.selectedRows[0]);
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
this.rootContainer = view.modelBuilder.flexContainer()
|
||||||
|
.withItems([this.table], {
|
||||||
|
flex: '1 1 auto',
|
||||||
|
order: 1
|
||||||
|
})
|
||||||
|
.withLayout(
|
||||||
|
{
|
||||||
|
flexFlow: 'column',
|
||||||
|
height: '100%',
|
||||||
|
})
|
||||||
|
.component();
|
||||||
|
|
||||||
|
this.detailsPanel = this.createDetailsPanel(view);
|
||||||
|
|
||||||
|
this.rootContainer.addItem(this.detailsPanel, {
|
||||||
|
flex: '0 0 200px',
|
||||||
|
order: 2,
|
||||||
|
CSSStyles: {
|
||||||
|
'padding-bottom': '15px',
|
||||||
|
'visibility': 'hidden'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
dispose() {
|
||||||
|
this.toDispose.forEach(disposable => disposable.dispose());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async displayResult(asmtResult: azdata.SqlAssessmentResult, method: AssessmentType) {
|
||||||
|
this.asmtType = method;
|
||||||
|
this.dataItems = asmtResult.items;
|
||||||
|
await this.table.updateProperties({
|
||||||
|
'data': asmtResult.items.map(item => this.convertToDataView(item))
|
||||||
|
});
|
||||||
|
this.rootContainer.setLayout({
|
||||||
|
flexFlow: 'column',
|
||||||
|
height: '100%',
|
||||||
|
});
|
||||||
|
this.rootContainer.setItemLayout(this.table, {
|
||||||
|
flex: '1 1 auto',
|
||||||
|
CSSStyles: {
|
||||||
|
'height': '100%'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
await this.table.updateProperties({
|
||||||
|
'height': '100%'
|
||||||
|
});
|
||||||
|
|
||||||
|
this.detailsPanel.updateCssStyles({
|
||||||
|
'visibility': 'hidden'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
public async appendResult(asmtResult: azdata.SqlAssessmentResult): Promise<void> {
|
||||||
|
if (this.dataItems) {
|
||||||
|
this.dataItems.push(...asmtResult.items);
|
||||||
|
}
|
||||||
|
|
||||||
|
await this.table.updateProperties({
|
||||||
|
'data': this.dataItems.map(item => this.convertToDataView(item))
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private async showDetails(rowNumber: number) {
|
||||||
|
const selectedRowValues = this.table.data[rowNumber];
|
||||||
|
const asmtResultItem = this.dataItems.find(item =>
|
||||||
|
item.targetName === selectedRowValues[this.targetColOrder]
|
||||||
|
&& item.checkId === selectedRowValues[this.checkIdColOrder]);
|
||||||
|
if (!asmtResultItem) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.checkNamePlaceholder.value = asmtResultItem.displayName;
|
||||||
|
this.checkDescriptionPlaceholder.value = asmtResultItem.description;
|
||||||
|
this.clickHereLabel.url = asmtResultItem.helpLink;
|
||||||
|
this.tagsPlaceholder.value = asmtResultItem.tags?.join(', ');
|
||||||
|
this.asmtMessagePlaceholder.value = asmtResultItem.message;
|
||||||
|
|
||||||
|
if (this.asmtType === AssessmentType.InvokeAssessment) {
|
||||||
|
this.asmtMessageDiv.display = 'block';
|
||||||
|
this.descriptionCaption.display = 'block';
|
||||||
|
} else {
|
||||||
|
this.asmtMessageDiv.display = 'none';
|
||||||
|
this.descriptionCaption.display = 'none';
|
||||||
|
}
|
||||||
|
|
||||||
|
this.detailsPanel.updateCssStyles({
|
||||||
|
'visibility': 'visible'
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
private createDetailsPanel(view: azdata.ModelView): azdata.FlexContainer {
|
||||||
|
|
||||||
|
const root = view.modelBuilder.flexContainer()
|
||||||
|
.withLayout({
|
||||||
|
flexFlow: 'column',
|
||||||
|
height: '200px',
|
||||||
|
}).withProperties({
|
||||||
|
CSSStyles: {
|
||||||
|
'padding': '20px',
|
||||||
|
'border-top': '3px solid rgb(221, 221, 221)'
|
||||||
|
}
|
||||||
|
}).component();
|
||||||
|
const cssNoMarginFloatLeft = { 'margin': '0px', 'float': 'left' };
|
||||||
|
const cssBlockCaption = { 'font-weight': 'bold', 'margin': '0px', 'display': 'block', 'padding-top': '5px' };
|
||||||
|
const flexSettings = '0 1 auto';
|
||||||
|
|
||||||
|
|
||||||
|
this.checkNamePlaceholder = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||||
|
CSSStyles: { ...cssNoMarginFloatLeft, 'font-weight': 'bold', 'font-size': '16px', 'padding-bottom': '5px', 'display': 'block' }
|
||||||
|
}).component();
|
||||||
|
this.checkDescriptionPlaceholder = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||||
|
CSSStyles: { ...cssNoMarginFloatLeft, 'padding-right': '2px' }
|
||||||
|
}).component();
|
||||||
|
this.clickHereLabel = view.modelBuilder.hyperlink().withProperties<azdata.HyperlinkComponentProperties>({
|
||||||
|
label: localize('asmt.details.clickHere', "Click here"),
|
||||||
|
url: '',
|
||||||
|
CSSStyles: cssNoMarginFloatLeft
|
||||||
|
}).component();
|
||||||
|
const toLearnMoreText = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||||
|
CSSStyles: { ...cssNoMarginFloatLeft, 'padding-left': '2px' },
|
||||||
|
value: localize('asmt.details.toLearnMore', " to learn more.")
|
||||||
|
}).component();
|
||||||
|
const tagsCaption = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||||
|
CSSStyles: cssBlockCaption,
|
||||||
|
value: LocalizedStrings.TAGS_COLUMN_NAME
|
||||||
|
}).component();
|
||||||
|
this.tagsPlaceholder = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||||
|
CSSStyles: cssNoMarginFloatLeft
|
||||||
|
}).component();
|
||||||
|
|
||||||
|
this.asmtMessagePlaceholder = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||||
|
CSSStyles: cssNoMarginFloatLeft
|
||||||
|
}).component();
|
||||||
|
|
||||||
|
this.descriptionCaption = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||||
|
CSSStyles: cssBlockCaption,
|
||||||
|
value: localize('asmt.details.ruleDescription', "Rule Description")
|
||||||
|
}).component();
|
||||||
|
|
||||||
|
root.addItem(
|
||||||
|
this.checkNamePlaceholder, { flex: flexSettings }
|
||||||
|
);
|
||||||
|
|
||||||
|
this.asmtMessageDiv = view.modelBuilder.divContainer().withItems([
|
||||||
|
view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||||
|
CSSStyles: cssBlockCaption,
|
||||||
|
value: localize('asmt.details.recommendation', "Recommendation")
|
||||||
|
}).component(),
|
||||||
|
this.asmtMessagePlaceholder
|
||||||
|
]).component();
|
||||||
|
|
||||||
|
root.addItem(
|
||||||
|
this.asmtMessageDiv,
|
||||||
|
{ flex: flexSettings }
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
root.addItem(
|
||||||
|
view.modelBuilder.divContainer().withItems([
|
||||||
|
this.descriptionCaption,
|
||||||
|
this.checkDescriptionPlaceholder,
|
||||||
|
this.clickHereLabel,
|
||||||
|
toLearnMoreText
|
||||||
|
]).component(),
|
||||||
|
{ flex: flexSettings }
|
||||||
|
);
|
||||||
|
|
||||||
|
root.addItem(
|
||||||
|
view.modelBuilder.divContainer().withItems([
|
||||||
|
tagsCaption,
|
||||||
|
this.tagsPlaceholder
|
||||||
|
]).component(),
|
||||||
|
{ flex: flexSettings }
|
||||||
|
);
|
||||||
|
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
private clearOutDefaultRuleset(tags: string[]): string[] {
|
||||||
|
let idx = tags.findIndex(item => item.toUpperCase() === 'DEFAULTRULESET');
|
||||||
|
if (idx > -1) {
|
||||||
|
tags.splice(idx, 1);
|
||||||
|
}
|
||||||
|
return tags;
|
||||||
|
}
|
||||||
|
|
||||||
|
private convertToDataView(asmtResult: azdata.SqlAssessmentResultItem): any[] {
|
||||||
|
return [
|
||||||
|
asmtResult.targetName,
|
||||||
|
asmtResult.level,
|
||||||
|
this.asmtType === AssessmentType.InvokeAssessment ? asmtResult.message : asmtResult.displayName,
|
||||||
|
this.clearOutDefaultRuleset(asmtResult.tags),
|
||||||
|
asmtResult.checkId
|
||||||
|
];
|
||||||
|
}
|
||||||
|
}
|
||||||
184
extensions/sql-assessment/src/engine.ts
Normal file
@@ -0,0 +1,184 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
import * as mssql from '../../mssql';
|
||||||
|
import * as azdata from 'azdata';
|
||||||
|
import { createHistoryFileName, readHistoryFileNames, getAssessmentDate, TargetWithChildren } from './utils';
|
||||||
|
import { promises as fs } from 'fs';
|
||||||
|
import { TelemetryReporter, SqlAssessmentTelemetryView, SqlTelemetryActions } from './telemetry';
|
||||||
|
|
||||||
|
export enum AssessmentType {
|
||||||
|
AvailableRules = 1,
|
||||||
|
InvokeAssessment = 2
|
||||||
|
}
|
||||||
|
|
||||||
|
export type OnResultCallback = (result: azdata.SqlAssessmentResult, assessmentType: AssessmentType, append: boolean) => void;
|
||||||
|
|
||||||
|
export interface SqlAssessmentRecord {
|
||||||
|
result: azdata.SqlAssessmentResult;
|
||||||
|
dateUpdated: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
interface SqlAssessmentResultInfo extends SqlAssessmentRecord {
|
||||||
|
connectionInfo: azdata.connection.ConnectionProfile
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
export class AssessmentEngine {
|
||||||
|
private sqlAssessment!: mssql.ISqlAssessmentService;
|
||||||
|
private connectionUri: string = '';
|
||||||
|
private connectionProfile!: azdata.connection.ConnectionProfile;
|
||||||
|
private lastInvokedResults!: SqlAssessmentResultInfo;
|
||||||
|
private historicalRecords!: SqlAssessmentRecord[];
|
||||||
|
|
||||||
|
|
||||||
|
constructor(service: mssql.ISqlAssessmentService) {
|
||||||
|
this.sqlAssessment = service;
|
||||||
|
}
|
||||||
|
|
||||||
|
public get isServerConnection(): boolean {
|
||||||
|
return !this.connectionProfile.databaseName || this.connectionProfile.databaseName === 'master';
|
||||||
|
}
|
||||||
|
public get databaseName(): string {
|
||||||
|
return this.connectionProfile.databaseName;
|
||||||
|
}
|
||||||
|
public get recentResult(): SqlAssessmentResultInfo {
|
||||||
|
return this.lastInvokedResults;
|
||||||
|
}
|
||||||
|
public get targetName(): string {
|
||||||
|
return this.isServerConnection ? this.connectionProfile.serverName : this.connectionProfile.databaseName;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
public async initialize(connectionId: string) {
|
||||||
|
this.connectionUri = await azdata.connection.getUriForConnection(connectionId);
|
||||||
|
this.connectionProfile = await azdata.connection.getCurrentConnection();
|
||||||
|
}
|
||||||
|
|
||||||
|
public async performAssessment(asmtType: AssessmentType, onResult: OnResultCallback): Promise<void> {
|
||||||
|
if (this.isServerConnection) {
|
||||||
|
await this.performServerAssessment(asmtType, onResult);
|
||||||
|
} else {
|
||||||
|
if (asmtType === AssessmentType.AvailableRules) {
|
||||||
|
TelemetryReporter.sendActionEvent(SqlAssessmentTelemetryView, SqlTelemetryActions.GetDatabaseAssessmentRules);
|
||||||
|
await onResult(await this.sqlAssessment.getAssessmentItems(this.connectionUri, azdata.sqlAssessment.SqlAssessmentTargetType.Database), asmtType, false);
|
||||||
|
} else {
|
||||||
|
TelemetryReporter.sendActionEvent(SqlAssessmentTelemetryView, SqlTelemetryActions.InvokeDatabaseAssessment);
|
||||||
|
const result = await this.sqlAssessment.assessmentInvoke(this.connectionUri, azdata.sqlAssessment.SqlAssessmentTargetType.Database);
|
||||||
|
|
||||||
|
this.lastInvokedResults = {
|
||||||
|
connectionInfo: this.connectionProfile,
|
||||||
|
dateUpdated: Date.now(),
|
||||||
|
result: result
|
||||||
|
};
|
||||||
|
|
||||||
|
await onResult(result, asmtType, false);
|
||||||
|
|
||||||
|
this.saveAssessment(this.databaseName, result);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (asmtType === AssessmentType.InvokeAssessment && this.historicalRecords !== undefined) {
|
||||||
|
this.historicalRecords.push({
|
||||||
|
result: this.lastInvokedResults.result,
|
||||||
|
dateUpdated: this.lastInvokedResults.dateUpdated
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public generateAssessmentScript(): Promise<azdata.ResultStatus> {
|
||||||
|
TelemetryReporter.sendActionEvent(SqlAssessmentTelemetryView, SqlTelemetryActions.ExportAssessmentResults);
|
||||||
|
return this.sqlAssessment.generateAssessmentScript(this.lastInvokedResults.result.items, '', '', azdata.TaskExecutionMode.script);
|
||||||
|
}
|
||||||
|
|
||||||
|
public async readHistory(): Promise<SqlAssessmentRecord[]> {
|
||||||
|
if (this.historicalRecords === undefined) {
|
||||||
|
await this.loadHistory();
|
||||||
|
}
|
||||||
|
|
||||||
|
return this.historicalRecords;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadHistory(): Promise<void> {
|
||||||
|
this.historicalRecords = [];
|
||||||
|
const files: TargetWithChildren[] = await readHistoryFileNames(this.targetName);
|
||||||
|
|
||||||
|
for (let nFileName = 0; nFileName < files.length; nFileName++) {
|
||||||
|
const file: TargetWithChildren = files[nFileName];
|
||||||
|
const content: string = await fs.readFile(file.target, 'utf8');
|
||||||
|
const result: azdata.SqlAssessmentResult = JSON.parse(content);
|
||||||
|
|
||||||
|
if (this.isServerConnection) {
|
||||||
|
for (let nChild = 0; nChild < file.children.length; nChild++) {
|
||||||
|
const childResult: azdata.SqlAssessmentResult = JSON.parse(await fs.readFile(file.children[nChild], 'utf8'));
|
||||||
|
result.items.push(...childResult.items);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const date = getAssessmentDate(file.target);
|
||||||
|
|
||||||
|
this.historicalRecords.push({
|
||||||
|
dateUpdated: date,
|
||||||
|
result: result
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async performServerAssessment(asmtType: AssessmentType, onResult: OnResultCallback): Promise<void> {
|
||||||
|
let databaseListRequest = azdata.connection.listDatabases(this.connectionProfile.connectionId);
|
||||||
|
|
||||||
|
let assessmentResult: azdata.SqlAssessmentResult;
|
||||||
|
if (AssessmentType.InvokeAssessment) {
|
||||||
|
TelemetryReporter.sendActionEvent(SqlAssessmentTelemetryView, SqlTelemetryActions.InvokeServerAssessment);
|
||||||
|
assessmentResult = await this.sqlAssessment.assessmentInvoke(this.connectionUri, azdata.sqlAssessment.SqlAssessmentTargetType.Server);
|
||||||
|
|
||||||
|
this.lastInvokedResults = {
|
||||||
|
connectionInfo: this.connectionProfile,
|
||||||
|
dateUpdated: Date.now(),
|
||||||
|
result: assessmentResult
|
||||||
|
};
|
||||||
|
this.saveAssessment(this.connectionProfile.serverName, assessmentResult);
|
||||||
|
} else {
|
||||||
|
TelemetryReporter.sendActionEvent(SqlAssessmentTelemetryView, SqlTelemetryActions.GetServerAssessmentRules);
|
||||||
|
assessmentResult = await this.sqlAssessment.getAssessmentItems(this.connectionUri, azdata.sqlAssessment.SqlAssessmentTargetType.Server);
|
||||||
|
}
|
||||||
|
|
||||||
|
await onResult(assessmentResult, asmtType, false);
|
||||||
|
|
||||||
|
let connectionProvider = azdata.dataprotocol.getProvider<azdata.ConnectionProvider>(
|
||||||
|
this.connectionProfile.providerId, azdata.DataProviderType.ConnectionProvider);
|
||||||
|
|
||||||
|
const dbList = await databaseListRequest;
|
||||||
|
|
||||||
|
for (let nDbName = 0; nDbName < dbList.length; nDbName++) {
|
||||||
|
const db = dbList[nDbName];
|
||||||
|
|
||||||
|
if (await connectionProvider.changeDatabase(this.connectionUri, db)) {
|
||||||
|
let assessmentResult = asmtType === AssessmentType.InvokeAssessment
|
||||||
|
? await this.sqlAssessment.assessmentInvoke(this.connectionUri, azdata.sqlAssessment.SqlAssessmentTargetType.Database)
|
||||||
|
: await this.sqlAssessment.getAssessmentItems(this.connectionUri, azdata.sqlAssessment.SqlAssessmentTargetType.Database);
|
||||||
|
|
||||||
|
if (assessmentResult?.items) {
|
||||||
|
if (asmtType === AssessmentType.InvokeAssessment) {
|
||||||
|
this.lastInvokedResults.result.items.push(...assessmentResult?.items);
|
||||||
|
this.saveAssessment(db, assessmentResult);
|
||||||
|
}
|
||||||
|
await onResult(assessmentResult, asmtType, true);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async saveAssessment(target: string, assessment: azdata.SqlAssessmentResult): Promise<void> {
|
||||||
|
try {
|
||||||
|
const fileName = await createHistoryFileName(target, this.lastInvokedResults.dateUpdated);
|
||||||
|
return fs.writeFile(fileName, JSON.stringify(assessment));
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
console.error(`error saving sql assessment history file: ${err}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
219
extensions/sql-assessment/src/htmlReportGenerator.ts
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
import { LocalizedStrings } from './localized';
|
||||||
|
import * as azdata from 'azdata';
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
import { htmlEscape } from './utils';
|
||||||
|
|
||||||
|
export class HTMLReportBuilder {
|
||||||
|
constructor(
|
||||||
|
private _assessmentResult: azdata.SqlAssessmentResult,
|
||||||
|
private _dateUpdated: number,
|
||||||
|
private _connectionInfo: azdata.connection.ConnectionProfile
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public async build(): Promise<string> {
|
||||||
|
const serverInfo = await azdata.connection.getServerInfo(this._connectionInfo.connectionId);
|
||||||
|
|
||||||
|
let mainContent = `
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<title>${LocalizedStrings.REPORT_TITLE}</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="header">
|
||||||
|
<div>${LocalizedStrings.REPORT_TITLE}</div>
|
||||||
|
</div>
|
||||||
|
<div style="font-style: italic;">${new Date(this._dateUpdated).toLocaleString(vscode.env.language)}</div>
|
||||||
|
${this.buildVersionDetails(serverInfo)}
|
||||||
|
<div style="margin-top: 20px;">
|
||||||
|
${this.buildResultsSection()}
|
||||||
|
</div>
|
||||||
|
${this.buildStyleSection()}
|
||||||
|
</body>
|
||||||
|
</html>`;
|
||||||
|
return mainContent;
|
||||||
|
}
|
||||||
|
|
||||||
|
private instanceName(serverInfo: azdata.ServerInfo): string {
|
||||||
|
const serverName = this._connectionInfo.serverName;
|
||||||
|
if (['local', '(local)'].indexOf(serverName.toLowerCase()) >= 0) {
|
||||||
|
|
||||||
|
return serverInfo !== undefined
|
||||||
|
? (<any>serverInfo)['machineName']
|
||||||
|
: serverName;
|
||||||
|
}
|
||||||
|
return serverName;
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildVersionDetails(serverInfo: azdata.ServerInfo): string {
|
||||||
|
return `
|
||||||
|
<div class="details">
|
||||||
|
<div>
|
||||||
|
<span>${LocalizedStrings.API_VERSION}: ${this._assessmentResult.apiVersion}</span><br />
|
||||||
|
<span>${LocalizedStrings.DEFAULT_RULESET_VERSION}: ${this._assessmentResult.items[0].rulesetVersion}</span>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<span>${LocalizedStrings.SECTION_TITLE_SQL_SERVER}: ${serverInfo.serverEdition} ${serverInfo.serverVersion}</span><br>
|
||||||
|
<span>${LocalizedStrings.SERVER_INSTANCENAME}: ${this.instanceName(serverInfo)}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildResultsSection(): string {
|
||||||
|
let resultByTarget: { [targetType: number]: { [targetName: string]: azdata.SqlAssessmentResultItem[] } } = [];
|
||||||
|
this._assessmentResult.items.forEach(resultItem => {
|
||||||
|
if (resultByTarget[resultItem.targetType] === undefined) {
|
||||||
|
resultByTarget[resultItem.targetType] = Object.create([]);
|
||||||
|
}
|
||||||
|
if (resultByTarget[resultItem.targetType][resultItem.targetName] === undefined) {
|
||||||
|
resultByTarget[resultItem.targetType][resultItem.targetName] = [];
|
||||||
|
}
|
||||||
|
resultByTarget[resultItem.targetType][resultItem.targetName].push(resultItem);
|
||||||
|
});
|
||||||
|
|
||||||
|
let result = '';
|
||||||
|
if (resultByTarget[azdata.sqlAssessment.SqlAssessmentTargetType.Server] !== undefined) {
|
||||||
|
Object.keys(resultByTarget[azdata.sqlAssessment.SqlAssessmentTargetType.Server]).forEach(instanceName => {
|
||||||
|
result += this.buildTargetAssessmentSection(resultByTarget[azdata.sqlAssessment.SqlAssessmentTargetType.Server][instanceName]);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (resultByTarget[azdata.sqlAssessment.SqlAssessmentTargetType.Database] !== undefined) {
|
||||||
|
Object.keys(resultByTarget[azdata.sqlAssessment.SqlAssessmentTargetType.Database]).forEach(dbName => {
|
||||||
|
result += this.buildTargetAssessmentSection(resultByTarget[azdata.sqlAssessment.SqlAssessmentTargetType.Database][dbName]);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildTargetAssessmentSection(targetResults: azdata.SqlAssessmentResultItem[]): string {
|
||||||
|
let content = `
|
||||||
|
<div>
|
||||||
|
<div class="target">${targetResults[0].targetType === azdata.sqlAssessment.SqlAssessmentTargetType.Server ? LocalizedStrings.RESULTS_FOR_INSTANCE : LocalizedStrings.RESULTS_FOR_DATABASE}: ${targetResults[0].targetName}</div>
|
||||||
|
${this.buildSeveritySection(LocalizedStrings.REPORT_ERROR, targetResults.filter(item => item.level === 'Error'))}
|
||||||
|
${this.buildSeveritySection(LocalizedStrings.REPORT_WARNING, targetResults.filter(item => item.level === 'Warning'))}
|
||||||
|
${this.buildSeveritySection(LocalizedStrings.REPORT_INFO, targetResults.filter(item => item.level === 'Information'))}
|
||||||
|
</div>`;
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
private buildSeveritySection(severityName: string, items: azdata.SqlAssessmentResultItem[]) {
|
||||||
|
if (items.length === 0) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
return `
|
||||||
|
<div class="severityBlock">
|
||||||
|
<div>${LocalizedStrings.REPORT_SEVERITY_MESSAGE(severityName, items.length)}</div>
|
||||||
|
<table>
|
||||||
|
<tr><th>${LocalizedStrings.MESSAGE_COLUMN_NAME}</th><th>${LocalizedStrings.HELP_LINK_COLUMN_NAME}</th><th>${LocalizedStrings.TAGS_COLUMN_NAME}</th><th>${LocalizedStrings.CHECKID_COLUMN_NAME}</th></tr>
|
||||||
|
${this.buildItemsRows(items)}
|
||||||
|
</table>
|
||||||
|
</div>`;
|
||||||
|
}
|
||||||
|
private buildItemsRows(items: azdata.SqlAssessmentResultItem[]): string {
|
||||||
|
let content = '';
|
||||||
|
items.forEach(item => {
|
||||||
|
content += `<tr>
|
||||||
|
<td>${htmlEscape(item.message)}</td>
|
||||||
|
<td><a href='${item.helpLink}' target='_blank;'>${LocalizedStrings.LEARN_MORE_LINK}</a></td>
|
||||||
|
<td>${this.formatTags(item.tags)}</td>
|
||||||
|
<td>${item.checkId}</td>
|
||||||
|
</tr>`;
|
||||||
|
});
|
||||||
|
return content;
|
||||||
|
}
|
||||||
|
private formatTags(tags: string[]): string {
|
||||||
|
return tags?.join(', ');
|
||||||
|
}
|
||||||
|
|
||||||
|
private buildStyleSection(): string {
|
||||||
|
return `
|
||||||
|
<style>
|
||||||
|
* {
|
||||||
|
color: #4a4a4a;
|
||||||
|
font-family: "Segoe WPC", "Segoe UI", sans-serif;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
margin: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
div {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.header>* {
|
||||||
|
font-size: 30px;
|
||||||
|
font-weight: bold;
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.target {
|
||||||
|
font-size: 1.7em;
|
||||||
|
}
|
||||||
|
|
||||||
|
table {
|
||||||
|
border-collapse: collapse;
|
||||||
|
width: 100%;
|
||||||
|
border: 1px solid silver;
|
||||||
|
table-layout: fixed;
|
||||||
|
}
|
||||||
|
|
||||||
|
table th:nth-child(1) {
|
||||||
|
width: 85%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table th:nth-child(2) {
|
||||||
|
width: 80px;
|
||||||
|
}
|
||||||
|
table th:nth-child(3) {
|
||||||
|
width: 200px;
|
||||||
|
}
|
||||||
|
table th:nth-child(4) {
|
||||||
|
width: 10%;
|
||||||
|
}
|
||||||
|
|
||||||
|
table td,
|
||||||
|
table th {
|
||||||
|
border-bottom: 1px solid silver;
|
||||||
|
border-right: 1px dotted silver;
|
||||||
|
padding: 3px 5px;
|
||||||
|
white-space: normal;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
table th {
|
||||||
|
background-color: silver;
|
||||||
|
}
|
||||||
|
|
||||||
|
div.severityBlock>div {
|
||||||
|
font-size: larger;
|
||||||
|
}
|
||||||
|
@media print {
|
||||||
|
body {
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
table th:nth-child(2),
|
||||||
|
table td:nth-child(2) {
|
||||||
|
display:none;
|
||||||
|
}
|
||||||
|
table th:nth-child(3) {
|
||||||
|
width: 120px;
|
||||||
|
}
|
||||||
|
table th:nth-child(4) {
|
||||||
|
width: 150px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
}
|
||||||
36
extensions/sql-assessment/src/localized.ts
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
import * as nls from 'vscode-nls';
|
||||||
|
const localize = nls.loadMessageBundle();
|
||||||
|
|
||||||
|
export const LocalizedStrings = {
|
||||||
|
SECTION_TITLE_API: localize('asmt.section.api.title', "API information"),
|
||||||
|
API_VERSION: localize('asmt.apiversion', "API Version"),
|
||||||
|
DEFAULT_RULESET_VERSION: localize('asmt.rulesetversion', "Default Ruleset"),
|
||||||
|
SECTION_TITLE_SQL_SERVER: localize('asmt.section.instance.title', "SQL Server Instance Details"),
|
||||||
|
SERVER_VERSION: localize('asmt.serverversion', "Version"),
|
||||||
|
SERVER_EDITION: localize('asmt.serveredition', "Edition"),
|
||||||
|
SERVER_INSTANCENAME: localize('asmt.instancename', "Instance Name"),
|
||||||
|
SERVER_OSVERSION: localize('asmt.osversion', "OS Version"),
|
||||||
|
TARGET_COLUMN_NAME: localize('asmt.column.target', "Target"),
|
||||||
|
SEVERITY_COLUMN_NAME: localize('asmt.column.severity', "Severity"),
|
||||||
|
MESSAGE_COLUMN_NAME: localize('asmt.column.message', "Message"),
|
||||||
|
CHECKID_COLUMN_NAME: localize('asmt.column.checkId', "Check ID"),
|
||||||
|
TAGS_COLUMN_NAME: localize('asmt.column.tags', "Tags"),
|
||||||
|
LEARN_MORE_LINK: localize('asmt.learnMore', "Learn More"),
|
||||||
|
REPORT_TITLE: localize('asmt.sqlReportTitle', "SQL Assessment Report"),
|
||||||
|
RESULTS_FOR_DATABASE: localize('asmt.sqlReport.resultForDatabase', "Results for database"),
|
||||||
|
RESULTS_FOR_INSTANCE: localize('asmt.sqlReport.resultForInstance', "Results for server"),
|
||||||
|
REPORT_ERROR: localize('asmt.sqlReport.Error', "Error"),
|
||||||
|
REPORT_WARNING: localize('asmt.sqlReport.Warning', "Warning"),
|
||||||
|
REPORT_INFO: localize('asmt.sqlReport.Info', "Information"),
|
||||||
|
HELP_LINK_COLUMN_NAME: localize('asmt.column.helpLink', "Help Link"),
|
||||||
|
REPORT_SEVERITY_MESSAGE: function (severity: string, count: number) {
|
||||||
|
return localize('asmt.sqlReport.severityMsg', "{0}: {1} item(s)", severity, count);
|
||||||
|
},
|
||||||
|
ASSESSMENT_TAB_NAME: 'Assessment',
|
||||||
|
HISTORY_TAB_NAME: 'History'
|
||||||
|
};
|
||||||
@@ -5,9 +5,16 @@
|
|||||||
|
|
||||||
import * as vscode from 'vscode';
|
import * as vscode from 'vscode';
|
||||||
|
|
||||||
export function activate(_context: vscode.ExtensionContext) {
|
import MainController from './maincontroller';
|
||||||
|
|
||||||
|
let mainController: MainController;
|
||||||
|
|
||||||
|
export function activate(context: vscode.ExtensionContext) {
|
||||||
|
mainController = new MainController(context);
|
||||||
|
mainController.activate();
|
||||||
}
|
}
|
||||||
|
|
||||||
// this method is called when your extension is deactivated
|
// this method is called when your extension is deactivated
|
||||||
export function deactivate(): void {
|
export function deactivate(): void {
|
||||||
|
mainController?.deactivate();
|
||||||
}
|
}
|
||||||
|
|||||||
62
extensions/sql-assessment/src/maincontroller.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
import * as azdata from 'azdata';
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
import * as mssql from '../../mssql';
|
||||||
|
import { SqlAssessmentMainTab } from './tabs/assessmentMainTab';
|
||||||
|
import { SqlAssessmentHistoryTab } from './tabs/historyTab';
|
||||||
|
import { AssessmentEngine } from './engine';
|
||||||
|
import { TelemetryReporter, SqlAssessmentTelemetryView } from './telemetry';
|
||||||
|
|
||||||
|
const tabName = 'data-management-asmt';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The main controller class that initializes the extension
|
||||||
|
*/
|
||||||
|
export default class MainController {
|
||||||
|
private extensionContext: vscode.ExtensionContext;
|
||||||
|
private sqlAssessment!: mssql.ISqlAssessmentService;
|
||||||
|
private toDispose: vscode.Disposable[] = [];
|
||||||
|
private engine!: AssessmentEngine;
|
||||||
|
|
||||||
|
public constructor(context: vscode.ExtensionContext) {
|
||||||
|
this.extensionContext = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
public deactivate(): void {
|
||||||
|
this.toDispose.forEach(disposable => disposable.dispose());
|
||||||
|
}
|
||||||
|
|
||||||
|
public async activate(): Promise<boolean> {
|
||||||
|
|
||||||
|
this.sqlAssessment = ((await vscode.extensions.getExtension(mssql.extension.name)?.activate() as mssql.IExtension)).sqlAssessment;
|
||||||
|
this.engine = new AssessmentEngine(this.sqlAssessment);
|
||||||
|
this.registerModelViewProvider();
|
||||||
|
TelemetryReporter.sendViewEvent(SqlAssessmentTelemetryView);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private registerModelViewProvider(): void {
|
||||||
|
azdata.ui.registerModelViewProvider(tabName, async (view) => {
|
||||||
|
await this.engine.initialize(view.connection.connectionId);
|
||||||
|
const mainTab = await new SqlAssessmentMainTab(this.extensionContext, this.engine).Create(view);
|
||||||
|
this.toDispose.push(mainTab);
|
||||||
|
const historyTab = await new SqlAssessmentHistoryTab(this.extensionContext, this.engine).Create(view) as SqlAssessmentHistoryTab;
|
||||||
|
this.toDispose.push(historyTab);
|
||||||
|
const tabbedPanel = view.modelBuilder.tabbedPanel()
|
||||||
|
.withTabs([mainTab, historyTab])
|
||||||
|
.withLayout({ showIcon: true, alwaysShowTabs: true })
|
||||||
|
.component();
|
||||||
|
this.toDispose.push(tabbedPanel.onTabChanged(async (id) => {
|
||||||
|
if (id === historyTab.id) {
|
||||||
|
await historyTab.refresh();
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
await view.initializeModel(tabbedPanel);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
305
extensions/sql-assessment/src/tabs/assessmentMainTab.ts
Normal file
@@ -0,0 +1,305 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
import * as azdata from 'azdata';
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
import * as nls from 'vscode-nls';
|
||||||
|
import { SqlAssessmentTab } from './sqlAssessmentTab';
|
||||||
|
import { AssessmentEngine, AssessmentType } from '../engine';
|
||||||
|
import { promises as fs } from 'fs';
|
||||||
|
import { suggestReportFile } from '../utils';
|
||||||
|
import { HTMLReportBuilder } from '../htmlReportGenerator';
|
||||||
|
import { AssessmentResultGrid } from '../assessmentResultGrid';
|
||||||
|
import { LocalizedStrings } from '../localized';
|
||||||
|
import { TelemetryReporter, SqlAssessmentTelemetryView, SqlTelemetryActions } from '../telemetry';
|
||||||
|
|
||||||
|
const localize = nls.loadMessageBundle();
|
||||||
|
|
||||||
|
export class SqlAssessmentMainTab extends SqlAssessmentTab {
|
||||||
|
private assessmentPropertiesContainer!: azdata.PropertiesContainerComponent;
|
||||||
|
private apiVersionPropItem: azdata.PropertiesContainerItem;
|
||||||
|
private defaultRulesetPropItem: azdata.PropertiesContainerItem;
|
||||||
|
private invokeAssessmentLabel: string = localize('invokeAssessmentLabelServer', "Invoke assessment");
|
||||||
|
private getItemsLabel: string = localize('getAssessmentItemsServer', "View applicable rules");
|
||||||
|
private btnExportAsScript!: azdata.ButtonComponent;
|
||||||
|
private btnHTMLExport!: azdata.ButtonComponent;
|
||||||
|
|
||||||
|
private engine: AssessmentEngine;
|
||||||
|
private toDispose: vscode.Disposable[] = [];
|
||||||
|
private resultGrid!: AssessmentResultGrid;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
public constructor(extensionContext: vscode.ExtensionContext, engine: AssessmentEngine) {
|
||||||
|
super(extensionContext, LocalizedStrings.ASSESSMENT_TAB_NAME, 'MainTab', {
|
||||||
|
dark: extensionContext.asAbsolutePath('resources/dark/server.svg'),
|
||||||
|
light: extensionContext.asAbsolutePath('resources/light/server.svg')
|
||||||
|
});
|
||||||
|
this.apiVersionPropItem = { displayName: LocalizedStrings.API_VERSION, value: '' };
|
||||||
|
this.defaultRulesetPropItem = { displayName: LocalizedStrings.DEFAULT_RULESET_VERSION, value: '' };
|
||||||
|
this.engine = engine;
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispose() {
|
||||||
|
this.toDispose.forEach(disposable => disposable.dispose());
|
||||||
|
}
|
||||||
|
|
||||||
|
async tabContent(view: azdata.ModelView): Promise<azdata.Component> {
|
||||||
|
|
||||||
|
if (!this.engine.isServerConnection) {
|
||||||
|
this.invokeAssessmentLabel = localize('invokeAssessmentLabelDatabase', "Invoke assessment for {0}", this.engine.databaseName);
|
||||||
|
this.getItemsLabel = localize('getAssessmentItemsDatabase', "View applicable rules for {0}", this.engine.databaseName);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.invokeAssessmentLabel = localize('invokeAssessmentLabelServer', "Invoke assessment");
|
||||||
|
this.getItemsLabel = localize('getAssessmentItemsServer', "View applicable rules");
|
||||||
|
}
|
||||||
|
|
||||||
|
let rootContainer = view.modelBuilder.flexContainer().withLayout(
|
||||||
|
{
|
||||||
|
flexFlow: 'column',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%',
|
||||||
|
|
||||||
|
}).component();
|
||||||
|
|
||||||
|
rootContainer.addItem(await this.createPropertiesSection(view), { flex: '0 0 auto' });
|
||||||
|
rootContainer.addItem(await this.createToolbar(view), {
|
||||||
|
flex: '0 0 auto', CSSStyles: {
|
||||||
|
'border-top': '3px solid rgb(221, 221, 221)',
|
||||||
|
'margin-top': '20px',
|
||||||
|
'height': '32px'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
this.resultGrid = new AssessmentResultGrid(view);
|
||||||
|
rootContainer.addItem(this.resultGrid.component, {
|
||||||
|
flex: '1 1 auto',
|
||||||
|
CSSStyles: {
|
||||||
|
'padding-bottom': '15px'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return rootContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createPropertiesSection(view: azdata.ModelView): Promise<azdata.FlexContainer> {
|
||||||
|
const serverInfo = await azdata.connection.getServerInfo(view.connection.connectionId);
|
||||||
|
const connectionProfile = await azdata.connection.getCurrentConnection();
|
||||||
|
|
||||||
|
const propertiesContainer = view.modelBuilder.flexContainer()
|
||||||
|
.withLayout({
|
||||||
|
flexFlow: 'row',
|
||||||
|
justifyContent: 'flex-start'
|
||||||
|
}).component();
|
||||||
|
|
||||||
|
const apiInformationContainer = view.modelBuilder.flexContainer()
|
||||||
|
.withLayout({
|
||||||
|
flexFlow: 'column',
|
||||||
|
alignContent: 'flex-start'
|
||||||
|
}).component();
|
||||||
|
apiInformationContainer.addItem(
|
||||||
|
view.modelBuilder.text().withProperties({ value: LocalizedStrings.SECTION_TITLE_API }).component(), {
|
||||||
|
CSSStyles: { 'font-size': 'larger' }
|
||||||
|
});
|
||||||
|
|
||||||
|
this.assessmentPropertiesContainer = view.modelBuilder.propertiesContainer()
|
||||||
|
.withProperties<azdata.PropertiesContainerComponentProperties>({
|
||||||
|
propertyItems: [
|
||||||
|
this.apiVersionPropItem,
|
||||||
|
this.defaultRulesetPropItem]
|
||||||
|
}).component();
|
||||||
|
|
||||||
|
apiInformationContainer.addItem(this.assessmentPropertiesContainer, {
|
||||||
|
CSSStyles: {
|
||||||
|
'margin-left': '20px'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const sqlServerContainer = view.modelBuilder.flexContainer()
|
||||||
|
.withLayout({
|
||||||
|
flexFlow: 'column',
|
||||||
|
alignContent: 'flex-start'
|
||||||
|
}).component();
|
||||||
|
sqlServerContainer.addItem(
|
||||||
|
view.modelBuilder.text().withProperties({ value: LocalizedStrings.SECTION_TITLE_SQL_SERVER }).component(), {
|
||||||
|
CSSStyles: { 'font-size': 'larger' }
|
||||||
|
});
|
||||||
|
sqlServerContainer.addItem(
|
||||||
|
view.modelBuilder.propertiesContainer()
|
||||||
|
.withProperties<azdata.PropertiesContainerComponentProperties>({
|
||||||
|
propertyItems: [
|
||||||
|
{ displayName: LocalizedStrings.SERVER_VERSION, value: serverInfo.serverVersion },
|
||||||
|
{ displayName: LocalizedStrings.SERVER_INSTANCENAME, value: connectionProfile.serverName },
|
||||||
|
{ displayName: LocalizedStrings.SERVER_EDITION, value: serverInfo.serverEdition },
|
||||||
|
{ displayName: LocalizedStrings.SERVER_OSVERSION, value: serverInfo.osVersion },
|
||||||
|
]
|
||||||
|
}).component(), {
|
||||||
|
CSSStyles: {
|
||||||
|
'margin-left': '20px'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
propertiesContainer.addItem(apiInformationContainer, { flex: '0 0 300px', CSSStyles: { 'margin-left': '10px' } });
|
||||||
|
propertiesContainer.addItem(sqlServerContainer, { flex: '1 1 auto' });
|
||||||
|
|
||||||
|
return propertiesContainer;
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createToolbar(view: azdata.ModelView): Promise<azdata.ToolbarContainer> {
|
||||||
|
|
||||||
|
const targetIconPath = this.engine.isServerConnection
|
||||||
|
? {
|
||||||
|
dark: this.extensionContext.asAbsolutePath('resources/dark/server.svg'),
|
||||||
|
light: this.extensionContext.asAbsolutePath('resources/light/server.svg')
|
||||||
|
} : {
|
||||||
|
dark: this.extensionContext.asAbsolutePath('resources/dark/database.svg'),
|
||||||
|
light: this.extensionContext.asAbsolutePath('resources/light/database.svg')
|
||||||
|
};
|
||||||
|
|
||||||
|
const btnInvokeAssessment = view.modelBuilder.button()
|
||||||
|
.withProperties<azdata.ButtonProperties>({
|
||||||
|
label: this.invokeAssessmentLabel,
|
||||||
|
iconPath: targetIconPath,
|
||||||
|
}).component();
|
||||||
|
const btnInvokeAssessmentLoading = view.modelBuilder.loadingComponent()
|
||||||
|
.withItem(btnInvokeAssessment)
|
||||||
|
.withProperties<azdata.LoadingComponentProperties>({
|
||||||
|
loadingText: this.invokeAssessmentLabel,
|
||||||
|
showText: true,
|
||||||
|
loading: false
|
||||||
|
}).component();
|
||||||
|
|
||||||
|
this.toDispose.push(btnInvokeAssessment.onDidClick(async () => {
|
||||||
|
btnInvokeAssessmentLoading.loading = true;
|
||||||
|
try {
|
||||||
|
await this.engine.performAssessment(AssessmentType.InvokeAssessment,
|
||||||
|
async (result: azdata.SqlAssessmentResult, assessmentType: AssessmentType, append: boolean) => {
|
||||||
|
if (append) {
|
||||||
|
await this.resultGrid.appendResult(result);
|
||||||
|
} else {
|
||||||
|
this.displayResults(result, assessmentType);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
btnInvokeAssessmentLoading.loading = false;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
const btnGetAssessmentItems = view.modelBuilder.button()
|
||||||
|
.withProperties<azdata.ButtonProperties>({
|
||||||
|
label: this.getItemsLabel,
|
||||||
|
iconPath: targetIconPath,
|
||||||
|
}).component();
|
||||||
|
const btnGetAssessmentItemsLoading = view.modelBuilder.loadingComponent()
|
||||||
|
.withItem(btnGetAssessmentItems)
|
||||||
|
.withProperties<azdata.LoadingComponentProperties>({
|
||||||
|
loadingText: this.getItemsLabel,
|
||||||
|
showText: true,
|
||||||
|
loading: false
|
||||||
|
}).component();
|
||||||
|
|
||||||
|
this.toDispose.push(btnGetAssessmentItems.onDidClick(async () => {
|
||||||
|
btnGetAssessmentItemsLoading.loading = true;
|
||||||
|
try {
|
||||||
|
await this.engine.performAssessment(AssessmentType.AvailableRules,
|
||||||
|
async (result: azdata.SqlAssessmentResult, assessmentType: AssessmentType, append: boolean) => {
|
||||||
|
if (append) {
|
||||||
|
await this.resultGrid.appendResult(result);
|
||||||
|
} else {
|
||||||
|
this.displayResults(result, assessmentType);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
finally {
|
||||||
|
btnGetAssessmentItemsLoading.loading = false;
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.btnExportAsScript = view.modelBuilder.button()
|
||||||
|
.withProperties<azdata.ButtonProperties>({
|
||||||
|
label: localize('btnExportAsScript', "Export as script"),
|
||||||
|
iconPath: {
|
||||||
|
dark: this.extensionContext.asAbsolutePath('resources/dark/newquery_inverse.svg'),
|
||||||
|
light: this.extensionContext.asAbsolutePath('resources/light/newquery.svg')
|
||||||
|
},
|
||||||
|
enabled: false
|
||||||
|
}).component();
|
||||||
|
this.toDispose.push(this.btnExportAsScript.onDidClick(async () => {
|
||||||
|
this.engine.generateAssessmentScript();
|
||||||
|
}));
|
||||||
|
|
||||||
|
this.btnHTMLExport = view.modelBuilder.button()
|
||||||
|
.withProperties<azdata.ButtonProperties>({
|
||||||
|
label: localize('btnGeneratehtmlreport', "Create HTML Report"),
|
||||||
|
iconPath: {
|
||||||
|
dark: this.extensionContext.asAbsolutePath('resources/dark/newquery_inverse.svg'),
|
||||||
|
light: this.extensionContext.asAbsolutePath('resources/light/newquery.svg')
|
||||||
|
},
|
||||||
|
enabled: false
|
||||||
|
}).component();
|
||||||
|
|
||||||
|
this.toDispose.push(this.btnHTMLExport.onDidClick(async () => {
|
||||||
|
TelemetryReporter.sendActionEvent(SqlAssessmentTelemetryView, SqlTelemetryActions.CreateHTMLReport);
|
||||||
|
const options: vscode.SaveDialogOptions = {
|
||||||
|
defaultUri: vscode.Uri.file(suggestReportFile(Date.now())),
|
||||||
|
filters: { 'HTML File': ['html'] }
|
||||||
|
};
|
||||||
|
|
||||||
|
const choosenPath = await vscode.window.showSaveDialog(options);
|
||||||
|
if (choosenPath !== undefined) {
|
||||||
|
const reportContent = await new HTMLReportBuilder(this.engine.recentResult.result,
|
||||||
|
this.engine.recentResult.dateUpdated,
|
||||||
|
this.engine.recentResult.connectionInfo).build();
|
||||||
|
await fs.writeFile(choosenPath.fsPath, reportContent);
|
||||||
|
if (await vscode.window.showInformationMessage(
|
||||||
|
localize('asmtaction.openReport', "Report has been saved. Do you want to open it?"),
|
||||||
|
localize('asmtaction.label.open', "Open"), localize('asmtaction.label.cancel', "Cancel")
|
||||||
|
) === localize('asmtaction.label.open', "Open")) {
|
||||||
|
vscode.env.openExternal(choosenPath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}));
|
||||||
|
|
||||||
|
|
||||||
|
let btnViewSamples = view.modelBuilder.button()
|
||||||
|
.withProperties<azdata.ButtonProperties>({
|
||||||
|
label: localize('btnViewSamples', "View all rules and learn more on GitHub"),
|
||||||
|
iconPath: {
|
||||||
|
dark: this.extensionContext.asAbsolutePath('resources/dark/configuredashboard_inverse.svg'),
|
||||||
|
light: this.extensionContext.asAbsolutePath('resources/light/configuredashboard.svg')
|
||||||
|
},
|
||||||
|
}).component();
|
||||||
|
|
||||||
|
this.toDispose.push(btnViewSamples.onDidClick(() => {
|
||||||
|
TelemetryReporter.sendActionEvent(SqlAssessmentTelemetryView, SqlTelemetryActions.LearnMoreAssessmentLink);
|
||||||
|
vscode.env.openExternal(vscode.Uri.parse('https://aka.ms/sql-assessment-api'));
|
||||||
|
}));
|
||||||
|
|
||||||
|
return view.modelBuilder.toolbarContainer()
|
||||||
|
.withToolbarItems(
|
||||||
|
[
|
||||||
|
{ component: btnInvokeAssessmentLoading },
|
||||||
|
{ component: btnGetAssessmentItemsLoading },
|
||||||
|
{ component: this.btnExportAsScript },
|
||||||
|
{ component: this.btnHTMLExport },
|
||||||
|
{ component: btnViewSamples }
|
||||||
|
]
|
||||||
|
).component();
|
||||||
|
}
|
||||||
|
|
||||||
|
private displayResults(result: azdata.SqlAssessmentResult, assessmentType: AssessmentType): void {
|
||||||
|
this.apiVersionPropItem.value = result.apiVersion;
|
||||||
|
this.defaultRulesetPropItem.value = result.items?.length > 0 ? result.items[0].rulesetVersion : '';
|
||||||
|
this.assessmentPropertiesContainer.propertyItems = [
|
||||||
|
this.apiVersionPropItem,
|
||||||
|
this.defaultRulesetPropItem
|
||||||
|
];
|
||||||
|
|
||||||
|
this.resultGrid.displayResult(result, assessmentType);
|
||||||
|
this.btnExportAsScript.enabled = this.btnHTMLExport.enabled = assessmentType === AssessmentType.InvokeAssessment;
|
||||||
|
}
|
||||||
|
}
|
||||||
125
extensions/sql-assessment/src/tabs/historyTab.ts
Normal file
@@ -0,0 +1,125 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
import * as azdata from 'azdata';
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
import * as nls from 'vscode-nls';
|
||||||
|
import { SqlAssessmentTab } from './sqlAssessmentTab';
|
||||||
|
import { AssessmentEngine, AssessmentType } from '../engine';
|
||||||
|
import { AssessmentResultGrid } from '../assessmentResultGrid';
|
||||||
|
import { LocalizedStrings } from '../localized';
|
||||||
|
import { TelemetryReporter, SqlAssessmentTelemetryView, SqlTelemetryActions } from '../telemetry';
|
||||||
|
|
||||||
|
const localize = nls.loadMessageBundle();
|
||||||
|
|
||||||
|
export class SqlAssessmentHistoryTab extends SqlAssessmentTab {
|
||||||
|
private engine: AssessmentEngine;
|
||||||
|
private toDispose: vscode.Disposable[] = [];
|
||||||
|
private summaryTable!: azdata.TableComponent;
|
||||||
|
private resultGrid!: AssessmentResultGrid;
|
||||||
|
|
||||||
|
public constructor(extensionContext: vscode.ExtensionContext, engine: AssessmentEngine) {
|
||||||
|
super(extensionContext, LocalizedStrings.HISTORY_TAB_NAME, 'HistoryTab', {
|
||||||
|
dark: extensionContext.asAbsolutePath('resources/dark/history.svg'),
|
||||||
|
light: extensionContext.asAbsolutePath('resources/light/history.svg')
|
||||||
|
});
|
||||||
|
|
||||||
|
this.engine = engine;
|
||||||
|
}
|
||||||
|
|
||||||
|
public dispose() {
|
||||||
|
this.toDispose.forEach(disposable => disposable.dispose());
|
||||||
|
}
|
||||||
|
|
||||||
|
async tabContent(view: azdata.ModelView): Promise<azdata.Component> {
|
||||||
|
TelemetryReporter.sendActionEvent(SqlAssessmentTelemetryView, SqlTelemetryActions.OpenHistory);
|
||||||
|
this.summaryTable = await this.createHistorySummaryTable(view);
|
||||||
|
|
||||||
|
const root = view.modelBuilder.flexContainer()
|
||||||
|
.withItems([this.summaryTable])
|
||||||
|
.withLayout({
|
||||||
|
flexFlow: 'column',
|
||||||
|
width: '100%',
|
||||||
|
height: '100%'
|
||||||
|
}).component();
|
||||||
|
|
||||||
|
const title = view.modelBuilder.text().withProperties<azdata.TextComponentProperties>({
|
||||||
|
value: '',
|
||||||
|
CSSStyles: { 'font-weight': 'bold', 'margin-block-start': '0px', 'margin-block-end': '0px', 'font-size': '20px', 'padding-left': '20px', 'padding-bottom': '20px' }
|
||||||
|
}).component();
|
||||||
|
|
||||||
|
const backLink = view.modelBuilder.hyperlink().withProperties<azdata.HyperlinkComponentProperties>({
|
||||||
|
label: localize('asmt.history.back', "<< Back"),
|
||||||
|
url: '',
|
||||||
|
CSSStyles: { 'text-decoration': 'none', 'width': '150px' }
|
||||||
|
}).component();
|
||||||
|
backLink.onDidClick(async () => {
|
||||||
|
this.resultGrid.dispose();
|
||||||
|
|
||||||
|
root.clearItems();
|
||||||
|
root.addItem(this.summaryTable, { flex: '1 1 auto' });
|
||||||
|
});
|
||||||
|
|
||||||
|
const infoPanel = view.modelBuilder.flexContainer()
|
||||||
|
.withLayout({
|
||||||
|
flexFlow: 'row'
|
||||||
|
}).withProperties<azdata.ComponentProperties>({
|
||||||
|
CSSStyles: {
|
||||||
|
'padding-left': '15px'
|
||||||
|
}
|
||||||
|
}).component();
|
||||||
|
infoPanel.addItem(backLink, { flex: '0 0 auto' });
|
||||||
|
infoPanel.addItem(title);
|
||||||
|
|
||||||
|
this.toDispose.push(this.summaryTable.onRowSelected(async () => {
|
||||||
|
if (this.summaryTable.selectedRows?.length === 1) {
|
||||||
|
let rowNumber: number = this.summaryTable.selectedRows[0];
|
||||||
|
const historyResult = (await this.engine.readHistory())[rowNumber];
|
||||||
|
|
||||||
|
root.clearItems();
|
||||||
|
|
||||||
|
this.resultGrid = new AssessmentResultGrid(view);
|
||||||
|
this.toDispose.push(this.resultGrid);
|
||||||
|
await view.initializeModel(title);
|
||||||
|
|
||||||
|
|
||||||
|
this.resultGrid.displayResult(historyResult.result, AssessmentType.InvokeAssessment);
|
||||||
|
title.value = localize('asmt.history.resultsTitle', "Assessment Results from {0}", new Date(historyResult.dateUpdated).toLocaleString());
|
||||||
|
root.addItem(infoPanel, { flex: `0 0 50px` });
|
||||||
|
root.addItem(this.resultGrid.component);
|
||||||
|
this.summaryTable.selectedRows = [];
|
||||||
|
}
|
||||||
|
|
||||||
|
}));
|
||||||
|
|
||||||
|
return root;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async refresh() {
|
||||||
|
const historicalRecords = await this.engine.readHistory();
|
||||||
|
this.summaryTable.data = historicalRecords.map(item => [
|
||||||
|
new Date(item.dateUpdated).toLocaleString(),
|
||||||
|
item.result.items.filter(i => i.level === 'Error')?.length,
|
||||||
|
item.result.items.filter(i => i.level === 'Warning')?.length,
|
||||||
|
item.result.items.filter(i => i.level === 'Information')?.length
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private async createHistorySummaryTable(view: azdata.ModelView): Promise<azdata.TableComponent> {
|
||||||
|
const cssHeader = 'no-borders align-with-header';
|
||||||
|
return view.modelBuilder.table()
|
||||||
|
.withProperties<azdata.TableComponentProperties>({
|
||||||
|
data: [],
|
||||||
|
columns: [
|
||||||
|
{ value: localize('asmt.history.summaryAsmtDate', "Assessment Date"), headerCssClass: cssHeader, width: 125 },
|
||||||
|
{ value: localize('asmt.history.summaryError', "Error"), headerCssClass: cssHeader, width: 100 },
|
||||||
|
{ value: localize('asmt.history.summaryWarning', "Warning"), headerCssClass: cssHeader, width: 100 },
|
||||||
|
{ value: localize('asmt.history.summaryInfo', "Information"), headerCssClass: cssHeader, width: 100 }
|
||||||
|
],
|
||||||
|
height: '100%',
|
||||||
|
width: '100%'
|
||||||
|
}).component();
|
||||||
|
}
|
||||||
|
}
|
||||||
35
extensions/sql-assessment/src/tabs/sqlAssessmentTab.ts
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
import * as azdata from 'azdata';
|
||||||
|
import * as vscode from 'vscode';
|
||||||
|
|
||||||
|
export abstract class SqlAssessmentTab implements azdata.Tab, vscode.Disposable {
|
||||||
|
title!: string;
|
||||||
|
content!: azdata.Component;
|
||||||
|
id!: string;
|
||||||
|
icon?: string | vscode.Uri | { light: string | vscode.Uri; dark: string | vscode.Uri; } | undefined;
|
||||||
|
|
||||||
|
protected extensionContext: vscode.ExtensionContext;
|
||||||
|
|
||||||
|
public constructor(extensionContext: vscode.ExtensionContext, title: string, id: string, icon: { light: string; dark: string }) {
|
||||||
|
this.title = title;
|
||||||
|
this.id = id;
|
||||||
|
this.icon = icon;
|
||||||
|
this.extensionContext = extensionContext;
|
||||||
|
}
|
||||||
|
public dispose() {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public async Create(view: azdata.ModelView): Promise<SqlAssessmentTab> {
|
||||||
|
this.content = await this.tabContent(view);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
abstract async tabContent(view: azdata.ModelView): Promise<azdata.Component>;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
23
extensions/sql-assessment/src/telemetry.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
import AdsTelemetryReporter from 'ads-extension-telemetry';
|
||||||
|
|
||||||
|
const packageJson = require('../package.json');
|
||||||
|
export const TelemetryReporter = new AdsTelemetryReporter(packageJson.name, packageJson.version, packageJson.aiKey);
|
||||||
|
|
||||||
|
export const SqlAssessmentTelemetryView = 'SqlAssessmentTab';
|
||||||
|
|
||||||
|
export enum SqlTelemetryActions {
|
||||||
|
InvokeServerAssessment = 'SqlAssessmentServerInvoke',
|
||||||
|
InvokeDatabaseAssessment = 'SqlAssessmentDatabaseInvoke',
|
||||||
|
GetServerAssessmentRules = 'SqlAssessmentServerGetRules',
|
||||||
|
GetDatabaseAssessmentRules = 'SqlAssessmentDatabaseGetRules',
|
||||||
|
ExportAssessmentResults = 'SqlAssessmentExportResult',
|
||||||
|
LearnMoreAssessmentLink = 'SqlAssessmentLearnMoreLink',
|
||||||
|
CreateHTMLReport = 'SqlAssessmentHTMLReport',
|
||||||
|
OpenHistory = 'SqlAssessmentOpenHistory',
|
||||||
|
}
|
||||||
|
|
||||||
98
extensions/sql-assessment/src/utils.ts
Normal file
@@ -0,0 +1,98 @@
|
|||||||
|
/*---------------------------------------------------------------------------------------------
|
||||||
|
* Copyright (c) Microsoft Corporation. All rights reserved.
|
||||||
|
* Licensed under the Source EULA. See License.txt in the project root for license information.
|
||||||
|
*--------------------------------------------------------------------------------------------*/
|
||||||
|
|
||||||
|
import * as path from 'path';
|
||||||
|
import * as os from 'os';
|
||||||
|
import { promises as fs, existsSync, readdirSync } from 'fs';
|
||||||
|
|
||||||
|
export type TargetWithChildren = { target: string, children: string[] };
|
||||||
|
|
||||||
|
export function suggestFileName(prefix: string, ext: string, date: number): string {
|
||||||
|
const fileName = `${prefix}${generateDefaultFileName(new Date(date))}${ext}`;
|
||||||
|
return path.join(os.homedir(), fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function suggestReportFile(date: number): string {
|
||||||
|
const fileName = `SqlAssessmentReport_${generateDefaultFileName(new Date(date))}.html`;
|
||||||
|
return path.join(os.homedir(), fileName);
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function createHistoryFileName(targetName: string, date: number): Promise<string> {
|
||||||
|
const fileName = `${targetName}_${generateDefaultFileName(new Date(date))}.json`;
|
||||||
|
const dirPath = path.join(os.homedir(), 'SqlAssessmentHistory');
|
||||||
|
|
||||||
|
if (!existsSync(dirPath)) {
|
||||||
|
await fs.mkdir(dirPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
return path.join(dirPath, escapeFileName(fileName));
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function readHistoryFileNames(targetName: string): Promise<TargetWithChildren[]> {
|
||||||
|
const dirPath = path.join(os.homedir(), 'SqlAssessmentHistory');
|
||||||
|
|
||||||
|
if (!existsSync(dirPath)) {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
const files: string[] = readdirSync(dirPath);
|
||||||
|
|
||||||
|
return files
|
||||||
|
.filter(file => file.startsWith(`${escapeFileName(targetName)}_`))
|
||||||
|
.map(targetFile => {
|
||||||
|
let result: TargetWithChildren = {
|
||||||
|
target: path.join(dirPath, targetFile),
|
||||||
|
children: []
|
||||||
|
};
|
||||||
|
|
||||||
|
const datePart = `_${targetFile.split('_')[1]}`;
|
||||||
|
result.children.push(...files.filter(f => f.endsWith(datePart)));
|
||||||
|
result.children = result.children.map(c => path.join(dirPath, c));
|
||||||
|
|
||||||
|
return result;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export function readHistoryFileName(fileName: string): string {
|
||||||
|
return path.join(os.homedir(), 'SqlAssessmentHistory', `${fileName}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getAssessmentDate(fileName: string): number {
|
||||||
|
const file = path.parse(fileName).name;
|
||||||
|
return extractDate(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
function extractDate(fileName: string): number {
|
||||||
|
const strDate: string = fileName.split('_')[1].split('.')[0];
|
||||||
|
const date = new Date(
|
||||||
|
Number(strDate.substr(0, 4)), // y
|
||||||
|
Number(strDate.substr(4, 2)) - 1, // m
|
||||||
|
Number(strDate.substr(6, 2)), // d
|
||||||
|
Number(strDate.substr(8, 2)), // h
|
||||||
|
Number(strDate.substr(10, 2)), // m
|
||||||
|
Number(strDate.substr(12, 2)) // s
|
||||||
|
);
|
||||||
|
return date.getTime() - date.getTimezoneOffset() * 60 * 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateDefaultFileName(resultDate: Date): string {
|
||||||
|
return `${resultDate.toISOString().replace(/-/g, '').replace('T', '').replace(/:/g, '').split('.')[0]}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function htmlEscape(html: string): string {
|
||||||
|
return html.replace(/[<|>|&|"]/g, function (match) {
|
||||||
|
switch (match) {
|
||||||
|
case '<': return '<';
|
||||||
|
case '>': return '>';
|
||||||
|
case '&': return '&';
|
||||||
|
case '"': return '"';
|
||||||
|
case '\'': return ''';
|
||||||
|
default: return match;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function escapeFileName(str: string): string {
|
||||||
|
return str.replace(/\*/g, '_');
|
||||||
|
}
|
||||||
@@ -2,7 +2,82 @@
|
|||||||
# yarn lockfile v1
|
# yarn lockfile v1
|
||||||
|
|
||||||
|
|
||||||
|
"ads-extension-telemetry@github:Charles-Gagnon/ads-extension-telemetry#0.1.0":
|
||||||
|
version "0.1.0"
|
||||||
|
resolved "https://codeload.github.com/Charles-Gagnon/ads-extension-telemetry/tar.gz/70c2fea10e9ff6e329c4c5ec0b77017ada514b6d"
|
||||||
|
dependencies:
|
||||||
|
vscode-extension-telemetry "0.1.1"
|
||||||
|
|
||||||
|
applicationinsights@1.0.8:
|
||||||
|
version "1.0.8"
|
||||||
|
resolved "https://registry.yarnpkg.com/applicationinsights/-/applicationinsights-1.0.8.tgz#db6e3d983cf9f9405fe1ee5ba30ac6e1914537b5"
|
||||||
|
integrity sha512-KzOOGdphOS/lXWMFZe5440LUdFbrLpMvh2SaRxn7BmiI550KAoSb2gIhiq6kJZ9Ir3AxRRztjhzif+e5P5IXIg==
|
||||||
|
dependencies:
|
||||||
|
diagnostic-channel "0.2.0"
|
||||||
|
diagnostic-channel-publishers "0.2.1"
|
||||||
|
zone.js "0.7.6"
|
||||||
|
|
||||||
|
diagnostic-channel-publishers@0.2.1:
|
||||||
|
version "0.2.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/diagnostic-channel-publishers/-/diagnostic-channel-publishers-0.2.1.tgz#8e2d607a8b6d79fe880b548bc58cc6beb288c4f3"
|
||||||
|
integrity sha1-ji1geottef6IC1SLxYzGvrKIxPM=
|
||||||
|
|
||||||
|
diagnostic-channel@0.2.0:
|
||||||
|
version "0.2.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/diagnostic-channel/-/diagnostic-channel-0.2.0.tgz#cc99af9612c23fb1fff13612c72f2cbfaa8d5a17"
|
||||||
|
integrity sha1-zJmvlhLCP7H/8TYSxy8sv6qNWhc=
|
||||||
|
dependencies:
|
||||||
|
semver "^5.3.0"
|
||||||
|
|
||||||
|
semver@^5.3.0:
|
||||||
|
version "5.7.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7"
|
||||||
|
integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ==
|
||||||
|
|
||||||
|
semver@^6.3.0:
|
||||||
|
version "6.3.0"
|
||||||
|
resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d"
|
||||||
|
integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==
|
||||||
|
|
||||||
|
vscode-extension-telemetry@0.1.1:
|
||||||
|
version "0.1.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/vscode-extension-telemetry/-/vscode-extension-telemetry-0.1.1.tgz#91387e06b33400c57abd48979b0e790415ae110b"
|
||||||
|
integrity sha512-TkKKG/B/J94DP5qf6xWB4YaqlhWDg6zbbqVx7Bz//stLQNnfE9XS1xm3f6fl24c5+bnEK0/wHgMgZYKIKxPeUA==
|
||||||
|
dependencies:
|
||||||
|
applicationinsights "1.0.8"
|
||||||
|
|
||||||
|
vscode-jsonrpc@^5.0.1:
|
||||||
|
version "5.0.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/vscode-jsonrpc/-/vscode-jsonrpc-5.0.1.tgz#9bab9c330d89f43fc8c1e8702b5c36e058a01794"
|
||||||
|
integrity sha512-JvONPptw3GAQGXlVV2utDcHx0BiY34FupW/kI6mZ5x06ER5DdPG/tXWMVHjTNULF5uKPOUUD0SaXg5QaubJL0A==
|
||||||
|
|
||||||
|
vscode-languageclient@^5.3.0-next.1:
|
||||||
|
version "5.3.0-next.9"
|
||||||
|
resolved "https://registry.yarnpkg.com/vscode-languageclient/-/vscode-languageclient-5.3.0-next.9.tgz#34f58017647f15cd86015f7af45935dc750611f7"
|
||||||
|
integrity sha512-BFA3X1y2EI2CfsSBy0KG2Xr5BOYfd/97jTmD+doqL6oj+cY8S7AmRCOwb2f9Hbjq8GWL7YC+OJ0leZEUSPgP0A==
|
||||||
|
dependencies:
|
||||||
|
semver "^6.3.0"
|
||||||
|
vscode-languageserver-protocol "^3.15.0-next.8"
|
||||||
|
|
||||||
|
vscode-languageserver-protocol@^3.15.0-next.8:
|
||||||
|
version "3.15.3"
|
||||||
|
resolved "https://registry.yarnpkg.com/vscode-languageserver-protocol/-/vscode-languageserver-protocol-3.15.3.tgz#3fa9a0702d742cf7883cb6182a6212fcd0a1d8bb"
|
||||||
|
integrity sha512-zrMuwHOAQRhjDSnflWdJG+O2ztMWss8GqUUB8dXLR/FPenwkiBNkMIJJYfSN6sgskvsF0rHAoBowNQfbyZnnvw==
|
||||||
|
dependencies:
|
||||||
|
vscode-jsonrpc "^5.0.1"
|
||||||
|
vscode-languageserver-types "3.15.1"
|
||||||
|
|
||||||
|
vscode-languageserver-types@3.15.1:
|
||||||
|
version "3.15.1"
|
||||||
|
resolved "https://registry.yarnpkg.com/vscode-languageserver-types/-/vscode-languageserver-types-3.15.1.tgz#17be71d78d2f6236d414f0001ce1ef4d23e6b6de"
|
||||||
|
integrity sha512-+a9MPUQrNGRrGU630OGbYVQ+11iOIovjCkqxajPa9w57Sd5ruK8WQNsslzpa0x/QJqC8kRc2DUxWjIFwoNm4ZQ==
|
||||||
|
|
||||||
vscode-nls@^3.2.1:
|
vscode-nls@^3.2.1:
|
||||||
version "3.2.5"
|
version "3.2.5"
|
||||||
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.5.tgz#25520c1955108036dec607c85e00a522f247f1a4"
|
resolved "https://registry.yarnpkg.com/vscode-nls/-/vscode-nls-3.2.5.tgz#25520c1955108036dec607c85e00a522f247f1a4"
|
||||||
integrity sha512-ITtoh3V4AkWXMmp3TB97vsMaHRgHhsSFPsUdzlueSL+dRZbSNTZeOmdQv60kjCV306ghPxhDeoNUEm3+EZMuyw==
|
integrity sha512-ITtoh3V4AkWXMmp3TB97vsMaHRgHhsSFPsUdzlueSL+dRZbSNTZeOmdQv60kjCV306ghPxhDeoNUEm3+EZMuyw==
|
||||||
|
|
||||||
|
zone.js@0.7.6:
|
||||||
|
version "0.7.6"
|
||||||
|
resolved "https://registry.yarnpkg.com/zone.js/-/zone.js-0.7.6.tgz#fbbc39d3e0261d0986f1ba06306eb3aeb0d22009"
|
||||||
|
integrity sha1-+7w50+AmHQmG8boGMG6zrrDSIAk=
|
||||||
|
|||||||
@@ -274,8 +274,10 @@ export default class TableComponent extends ComponentBase<azdata.TableComponentP
|
|||||||
|
|
||||||
Object.keys(this._checkboxColumns).forEach(col => this.registerPlugins(col, this._checkboxColumns[col]));
|
Object.keys(this._checkboxColumns).forEach(col => this.registerPlugins(col, this._checkboxColumns[col]));
|
||||||
Object.keys(this._buttonsColumns).forEach(col => this.registerPlugins(col, this._buttonsColumns[col]));
|
Object.keys(this._buttonsColumns).forEach(col => this.registerPlugins(col, this._buttonsColumns[col]));
|
||||||
|
|
||||||
if (this.headerFilter === true) {
|
if (this.headerFilter === true) {
|
||||||
this.registerFilterPlugin();
|
this.registerFilterPlugin();
|
||||||
|
this._tableData.clearFilter();
|
||||||
}
|
}
|
||||||
if (this.ariaRowCount === -1) {
|
if (this.ariaRowCount === -1) {
|
||||||
this._table.removeAriaRowCount();
|
this._table.removeAriaRowCount();
|
||||||
@@ -393,6 +395,7 @@ export default class TableComponent extends ComponentBase<azdata.TableComponentP
|
|||||||
if (filterValues) {
|
if (filterValues) {
|
||||||
this._tableData.filter();
|
this._tableData.filter();
|
||||||
this._table.grid.resetActiveCell();
|
this._table.grid.resetActiveCell();
|
||||||
|
this.data = this._tableData.getItems().map(dataObject => Object.values(dataObject));
|
||||||
this.layoutTable();
|
this.layoutTable();
|
||||||
} else {
|
} else {
|
||||||
this._tableData.clearFilter();
|
this._tableData.clearFilter();
|
||||||
|
|||||||