mirror of
https://github.com/elastic/kibana.git
synced 2025-04-23 17:28:26 -04:00
[Observability] Exploratory View initial skeleton (#94426)
Co-authored-by: Kibana Machine <42973632+kibanamachine@users.noreply.github.com>
This commit is contained in:
parent
76acef0c2c
commit
a640522140
82 changed files with 6057 additions and 54 deletions
|
@ -83,6 +83,7 @@
|
|||
"x-pack/plugins/uptime/server/lib/requests/helper.ts",
|
||||
"x-pack/plugins/uptime/public/lib/helper/rtl_helpers.tsx",
|
||||
"x-pack/plugins/uptime/public/lib/helper/enzyme_helpers.tsx",
|
||||
"x-pack/plugins/observability/public/components/shared/exploratory_view/rtl_helpers.tsx",
|
||||
"x-pack/plugins/apm/server/utils/test_helpers.tsx",
|
||||
"x-pack/plugins/apm/public/utils/testHelpers.tsx",
|
||||
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
"version": "8.0.0",
|
||||
"kibanaVersion": "kibana",
|
||||
"configPath": ["xpack", "observability"],
|
||||
"optionalPlugins": ["licensing", "home", "usageCollection"],
|
||||
"optionalPlugins": ["licensing", "home", "usageCollection","lens"],
|
||||
"requiredPlugins": ["data"],
|
||||
"ui": true,
|
||||
"server": true,
|
||||
|
|
|
@ -0,0 +1,116 @@
|
|||
<svg width="568" height="320" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#25262E" d="M0 0h568v320H0z"/>
|
||||
<g filter="url(#kibana_dashboard_dark__filter0_d)">
|
||||
<rect x="24" y="172" width="248" height="124" rx="4" fill="#1D1E24"/>
|
||||
</g>
|
||||
<rect x="32" y="180" width="120" height="4" rx="2" fill="#A7AFBE"/>
|
||||
<rect x="40" y="200" width="16" height="4" rx="2" fill="#535966"/>
|
||||
<rect x="72" y="284" width="24" height="4" rx="2" fill="#535966"/>
|
||||
<rect x="125" y="284" width="24" height="4" rx="2" fill="#535966"/>
|
||||
<rect x="178" y="284" width="24" height="4" rx="2" fill="#535966"/>
|
||||
<rect x="231" y="284" width="24" height="4" rx="2" fill="#535966"/>
|
||||
<rect x="32" y="221" width="24" height="4" rx="2" fill="#535966"/>
|
||||
<rect x="36" y="242" width="20" height="4" rx="2" fill="#535966"/>
|
||||
<rect x="40" y="263" width="16" height="4" rx="2" transform="rotate(-.17 40 263)" fill="#535966"/>
|
||||
<path d="M64.5 192.5v9.5m199 73.5H243M64.5 202h-4m4 0v21m0 0h-4m4 0v21m0 0h-4m4 0v21m0 0v10.5H84M64.5 265h-4M84 279.5v-4m0 0h53m0 0v4m0-4h53m0 0v4m0-4h53m0 0v4" stroke="#343741" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<rect x="67" y="194" width="181" height="16" rx="1" fill="#54B399"/>
|
||||
<rect x="67" y="236" width="59" height="16" rx="1" fill="#D36086"/>
|
||||
<rect x="67" y="215" width="120" height="16" rx="1" fill="#6092C0"/>
|
||||
<rect x="67" y="257" width="30" height="16" rx="1" fill="#9170B8"/>
|
||||
<g filter="url(#kibana_dashboard_dark__filter1_d)">
|
||||
<rect x="24" y="24" width="520" height="124" rx="4" fill="#1D1E24"/>
|
||||
</g>
|
||||
<rect x="32" y="32" width="101" height="4" rx="2" fill="#A7AFBE"/>
|
||||
<rect x="38" y="52" width="18" height="4" rx="2" fill="#535966"/>
|
||||
<rect x="32" y="73" width="24" height="4" rx="2" fill="#535966"/>
|
||||
<rect x="40" y="94" width="16" height="4" rx="2" fill="#535966"/>
|
||||
<rect x="36" y="115" width="20" height="4" rx="2" fill="#535966"/>
|
||||
<rect x="72" y="136" width="24" height="4" rx="2" fill="#535966"/>
|
||||
<rect x="216" y="136" width="24" height="4" rx="2" fill="#535966"/>
|
||||
<rect x="360" y="136" width="24" height="4" rx="2" fill="#535966"/>
|
||||
<rect x="504" y="136" width="24" height="4" rx="2" fill="#535966"/>
|
||||
<path opacity=".1" d="M80.177 118L65 110v17h470.5V90.5l-15.177 12.692-15.178 1.346-15.177-8.846-15.178 17.885-15.177.961-15.178-5.384-15.177-.769-15.177 5.384-15.178-5.846-15.177-40.615-15.178 18.077-15.177-38.462L338.194 104l-15.178-16.308-15.177 12.923-15.178-9.538-15.177 5.615-15.178-24.384L247.129 71l-15.177-15.615-15.178 49.077-15.177 4.615-15.178-22.154-15.177-5.385L156.065 50l-15.178 37.923-15.177 6.923-15.178 19.692-15.177-4.692L80.177 118z" fill="#54B399"/>
|
||||
<path d="M65 110l15.177 8 15.178-8.154 15.177 4.692 15.178-19.692 15.177-6.923L156.065 50l15.177 31.538 15.177 5.385 15.178 22.154 15.177-4.615 15.178-49.077L247.129 71l15.177 1.308 15.178 24.384 15.177-5.615 15.178 9.538 15.177-12.923L338.194 104l15.177-57.077 15.177 38.462 15.178-18.077 15.177 40.615 15.178 5.846 15.177-5.384 15.177.769 15.178 5.384 15.177-.961 15.178-17.885 15.177 8.846 15.178-1.346L535.5 90.5" stroke="#54B399" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle opacity=".1" cx="100" cy="63" r="8" fill="#6092C0"/>
|
||||
<circle opacity=".1" cx="232" cy="55" r="8" fill="#6092C0"/>
|
||||
<circle opacity=".1" cx="323" cy="88" r="8" fill="#6092C0"/>
|
||||
<circle opacity=".1" cx="353" cy="47" r="7.5" fill="#6092C0" stroke="#6092C0"/>
|
||||
<circle opacity=".1" cx="523" cy="47" r="8" fill="#6092C0"/>
|
||||
<circle opacity=".1" cx="106" cy="68" r="4" fill="#6092C0"/>
|
||||
<circle opacity=".1" cx="156" cy="50" r="4" fill="#6092C0"/>
|
||||
<circle opacity=".1" cx="240" cy="55" r="4" fill="#6092C0"/>
|
||||
<circle opacity=".1" cx="262" cy="72" r="4" fill="#6092C0"/>
|
||||
<circle opacity=".1" cx="345" cy="48" r="4" fill="#6092C0"/>
|
||||
<circle opacity=".1" cx="484" cy="92" r="3.5" fill="#6092C0" stroke="#6092C0"/>
|
||||
<circle opacity=".1" cx="490" cy="96" r="4" fill="#6092C0"/>
|
||||
<circle cx="100" cy="63" r="7.5" stroke="#6092C0"/>
|
||||
<circle cx="232" cy="55" r="7.5" stroke="#6092C0"/>
|
||||
<circle cx="323" cy="88" r="7.5" stroke="#6092C0"/>
|
||||
<circle cx="353" cy="47" r="7.5" stroke="#6092C0"/>
|
||||
<circle cx="523" cy="47" r="7.5" stroke="#6092C0"/>
|
||||
<circle cx="106" cy="68" r="3.5" stroke="#6092C0"/>
|
||||
<circle cx="156" cy="50" r="3.5" stroke="#6092C0"/>
|
||||
<circle cx="240" cy="55" r="3.5" stroke="#6092C0"/>
|
||||
<circle cx="262" cy="72" r="3.5" stroke="#6092C0"/>
|
||||
<circle cx="345" cy="48" r="3.5" stroke="#6092C0"/>
|
||||
<circle cx="484" cy="92" r="3.5" stroke="#6092C0"/>
|
||||
<circle cx="490" cy="96" r="3.5" stroke="#6092C0"/>
|
||||
<path d="M64.5 44.5V54m471 73.5H516M64.5 54h-4m4 0v21m0 0h-4m4 0v21m0 0h-4m4 0v21m0 0v10.5H84M64.5 117h-4M84 131.5v-4m0 0h144m0 0v4m0-4h144m0 0v4m0-4h144m0 0v4" stroke="#343741" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<g filter="url(#kibana_dashboard_dark__filter2_d)">
|
||||
<rect x="296" y="172" width="248" height="124" rx="4" fill="#1D1E24"/>
|
||||
</g>
|
||||
<rect x="304" y="180" width="80" height="4" rx="2" fill="#A7AFBE"/>
|
||||
<path d="M448.284 268.284a39.997 39.997 0 00-10.054-63.888 39.997 39.997 0 00-24.383-3.92l2.461 15.81a24 24 0 0120.663 40.685l11.313 11.313z" fill="#54B399"/>
|
||||
<mask id="a" maskUnits="userSpaceOnUse" x="385" y="250" width="72" height="31" fill="#000">
|
||||
<path fill="#fff" d="M385 250h72v31h-72z"/>
|
||||
<path d="M386.416 261.728a39.996 39.996 0 0034.591 18.259 39.994 39.994 0 0033.628-19.976l-13.854-8.005a24 24 0 01-40.932 1.031l-13.433 8.691z"/>
|
||||
</mask>
|
||||
<path d="M386.416 261.728a39.996 39.996 0 0034.591 18.259 39.994 39.994 0 0033.628-19.976l-13.854-8.005a24 24 0 01-40.932 1.031l-13.433 8.691z" fill="#6092C0"/>
|
||||
<path d="M386.416 261.728a39.996 39.996 0 0034.591 18.259 39.994 39.994 0 0033.628-19.976l-13.854-8.005a24 24 0 01-40.932 1.031l-13.433 8.691z" stroke="#1D1E24" stroke-width="2" mask="url(#a)"/>
|
||||
<mask id="b" maskUnits="userSpaceOnUse" x="379" y="213" width="24" height="54" fill="#000">
|
||||
<path fill="#fff" d="M379 213h24v54h-24z"/>
|
||||
<path d="M388.645 215.163a40 40 0 00.54 50.341l12.326-10.202a24 24 0 01-.324-30.204l-12.542-9.935z"/>
|
||||
</mask>
|
||||
<path d="M388.645 215.163a40 40 0 00.54 50.341l12.326-10.202a24 24 0 01-.324-30.204l-12.542-9.935z" fill="#D36086"/>
|
||||
<path d="M388.645 215.163a40 40 0 00.54 50.341l12.326-10.202a24 24 0 01-.324-30.204l-12.542-9.935z" stroke="#1D1E24" stroke-width="2" mask="url(#b)"/>
|
||||
<mask id="c" maskUnits="userSpaceOnUse" x="384" y="199" width="37" height="30" fill="#000">
|
||||
<path fill="#fff" d="M384 199h37v30h-37z"/>
|
||||
<path d="M420 200a40.005 40.005 0 00-33.929 18.814l13.572 8.474A24.004 24.004 0 01420 216v-16z"/>
|
||||
</mask>
|
||||
<path d="M420 200a40.005 40.005 0 00-33.929 18.814l13.572 8.474A24.004 24.004 0 01420 216v-16z" fill="#9170B8"/>
|
||||
<path d="M420 200a40.005 40.005 0 00-33.929 18.814l13.572 8.474A24.004 24.004 0 01420 216v-16z" stroke="#1D1E24" stroke-width="2" mask="url(#c)"/>
|
||||
<rect x="367" y="192" width="22" height="4" rx="2" fill="#535966"/>
|
||||
<rect x="345" y="250" width="16" height="4" rx="2" fill="#535966"/>
|
||||
<rect x="441" y="284" width="24" height="4" rx="2" fill="#535966"/>
|
||||
<rect x="471" y="210" width="24" height="4" rx="2" fill="#535966"/>
|
||||
<path d="M393.5 194h4l4 8.5m65 9.5h-4l-7 4.5m-26.5 64l3.5 5.5h4m-58-41l-9 7h-4" stroke="#343741" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<defs>
|
||||
<filter id="kibana_dashboard_dark__filter0_d" x="8" y="160" width="280" height="156" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dy="4"/>
|
||||
<feGaussianBlur stdDeviation="8"/>
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0"/>
|
||||
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feBlend in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<filter id="kibana_dashboard_dark__filter1_d" x="8" y="12" width="552" height="156" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dy="4"/>
|
||||
<feGaussianBlur stdDeviation="8"/>
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0"/>
|
||||
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feBlend in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<filter id="kibana_dashboard_dark__filter2_d" x="280" y="160" width="280" height="156" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dy="4"/>
|
||||
<feGaussianBlur stdDeviation="8"/>
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0"/>
|
||||
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feBlend in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 9 KiB |
|
@ -0,0 +1,116 @@
|
|||
<svg width="568" height="320" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||
<path fill="#F5F7FA" d="M0 0h568v320H0z"/>
|
||||
<g filter="url(#kibana_dashboard_light__filter0_d)">
|
||||
<rect x="24" y="172" width="248" height="124" rx="4" fill="#fff"/>
|
||||
</g>
|
||||
<rect x="32" y="180" width="120" height="4" rx="2" fill="#6A717D"/>
|
||||
<rect x="40" y="200" width="16" height="4" rx="2" fill="#98A2B3"/>
|
||||
<rect x="72" y="284" width="24" height="4" rx="2" fill="#98A2B3"/>
|
||||
<rect x="125" y="284" width="24" height="4" rx="2" fill="#98A2B3"/>
|
||||
<rect x="178" y="284" width="24" height="4" rx="2" fill="#98A2B3"/>
|
||||
<rect x="231" y="284" width="24" height="4" rx="2" fill="#98A2B3"/>
|
||||
<rect x="32" y="221" width="24" height="4" rx="2" fill="#98A2B3"/>
|
||||
<rect x="36" y="242" width="20" height="4" rx="2" fill="#98A2B3"/>
|
||||
<rect x="40" y="263" width="16" height="4" rx="2" transform="rotate(-.17 40 263)" fill="#98A2B3"/>
|
||||
<path d="M64.5 192.5v9.5m199 73.5H243M64.5 202h-4m4 0v21m0 0h-4m4 0v21m0 0h-4m4 0v21m0 0v10.5H84M64.5 265h-4M84 279.5v-4m0 0h53m0 0v4m0-4h53m0 0v4m0-4h53m0 0v4" stroke="#D3DAE6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<rect x="67" y="194" width="181" height="16" rx="1" fill="#54B399"/>
|
||||
<rect x="67" y="236" width="59" height="16" rx="1" fill="#D36086"/>
|
||||
<rect x="67" y="215" width="120" height="16" rx="1" fill="#6092C0"/>
|
||||
<rect x="67" y="257" width="30" height="16" rx="1" fill="#9170B8"/>
|
||||
<g filter="url(#kibana_dashboard_light__filter1_d)">
|
||||
<rect x="24" y="24" width="520" height="124" rx="4" fill="#fff"/>
|
||||
</g>
|
||||
<rect x="32" y="32" width="101" height="4" rx="2" fill="#6A717D"/>
|
||||
<rect x="38" y="52" width="18" height="4" rx="2" fill="#98A2B3"/>
|
||||
<rect x="32" y="73" width="24" height="4" rx="2" fill="#98A2B3"/>
|
||||
<rect x="40" y="94" width="16" height="4" rx="2" fill="#98A2B3"/>
|
||||
<rect x="36" y="115" width="20" height="4" rx="2" fill="#98A2B3"/>
|
||||
<rect x="72" y="136" width="24" height="4" rx="2" fill="#98A2B3"/>
|
||||
<rect x="216" y="136" width="24" height="4" rx="2" fill="#98A2B3"/>
|
||||
<rect x="360" y="136" width="24" height="4" rx="2" fill="#98A2B3"/>
|
||||
<rect x="504" y="136" width="24" height="4" rx="2" fill="#98A2B3"/>
|
||||
<path opacity=".1" d="M80.177 118L65 110v17h470.5V90.5l-15.177 12.692-15.178 1.346-15.177-8.846-15.178 17.885-15.177.961-15.178-5.384-15.177-.769-15.177 5.384-15.178-5.846-15.177-40.615-15.178 18.077-15.177-38.462L338.194 104l-15.178-16.308-15.177 12.923-15.178-9.538-15.177 5.615-15.178-24.384L247.129 71l-15.177-15.615-15.178 49.077-15.177 4.615-15.178-22.154-15.177-5.385L156.065 50l-15.178 37.923-15.177 6.923-15.178 19.692-15.177-4.692L80.177 118z" fill="#54B399"/>
|
||||
<path d="M65 110l15.177 8 15.178-8.154 15.177 4.692 15.178-19.692 15.177-6.923L156.065 50l15.177 31.538 15.177 5.385 15.178 22.154 15.177-4.615 15.178-49.077L247.129 71l15.177 1.308 15.178 24.384 15.177-5.615 15.178 9.538 15.177-12.923L338.194 104l15.177-57.077 15.177 38.462 15.178-18.077 15.177 40.615 15.178 5.846 15.177-5.384 15.177.769 15.178 5.384 15.177-.961 15.178-17.885 15.177 8.846 15.178-1.346L535.5 90.5" stroke="#54B399" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle opacity=".1" cx="100" cy="63" r="7.5" fill="#6092C0" stroke="#6092C0"/>
|
||||
<circle opacity=".1" cx="232" cy="55" r="7.5" fill="#6092C0" stroke="#6092C0"/>
|
||||
<circle opacity=".1" cx="323" cy="88" r="7.5" fill="#6092C0" stroke="#6092C0"/>
|
||||
<circle opacity=".1" cx="353" cy="47" r="7.5" fill="#6092C0" stroke="#6092C0"/>
|
||||
<circle opacity=".1" cx="523" cy="47" r="7.5" fill="#6092C0" stroke="#6092C0"/>
|
||||
<circle opacity=".1" cx="106" cy="68" r="3.5" fill="#6092C0" stroke="#6092C0"/>
|
||||
<circle opacity=".1" cx="156" cy="50" r="3.5" fill="#6092C0" stroke="#6092C0"/>
|
||||
<circle opacity=".1" cx="240" cy="55" r="3.5" fill="#6092C0" stroke="#6092C0"/>
|
||||
<circle opacity=".1" cx="262" cy="72" r="3.5" fill="#6092C0" stroke="#6092C0"/>
|
||||
<circle opacity=".1" cx="345" cy="48" r="3.5" fill="#6092C0" stroke="#6092C0"/>
|
||||
<circle opacity=".1" cx="484" cy="92" r="3.5" fill="#6092C0" stroke="#6092C0"/>
|
||||
<circle opacity=".1" cx="490" cy="96" r="3.5" fill="#6092C0" stroke="#6092C0"/>
|
||||
<circle cx="100" cy="63" r="7.5" stroke="#6092C0"/>
|
||||
<circle cx="232" cy="55" r="7.5" stroke="#6092C0"/>
|
||||
<circle cx="323" cy="88" r="7.5" stroke="#6092C0"/>
|
||||
<circle cx="353" cy="47" r="7.5" stroke="#6092C0"/>
|
||||
<circle cx="523" cy="47" r="7.5" stroke="#6092C0"/>
|
||||
<circle cx="106" cy="68" r="3.5" stroke="#6092C0"/>
|
||||
<circle cx="156" cy="50" r="3.5" stroke="#6092C0"/>
|
||||
<circle cx="240" cy="55" r="3.5" stroke="#6092C0"/>
|
||||
<circle cx="262" cy="72" r="3.5" stroke="#6092C0"/>
|
||||
<circle cx="345" cy="48" r="3.5" stroke="#6092C0"/>
|
||||
<circle cx="484" cy="92" r="3.5" stroke="#6092C0"/>
|
||||
<circle cx="490" cy="96" r="3.5" stroke="#6092C0"/>
|
||||
<path d="M64.5 44.5V54m471 73.5H516M64.5 54h-4m4 0v21m0 0h-4m4 0v21m0 0h-4m4 0v21m0 0v10.5H84M64.5 117h-4M84 131.5v-4m0 0h144m0 0v4m0-4h144m0 0v4m0-4h144m0 0v4" stroke="#D3DAE6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<g filter="url(#kibana_dashboard_light__filter2_d)">
|
||||
<rect x="296" y="172" width="248" height="124" rx="4" fill="#fff"/>
|
||||
</g>
|
||||
<rect x="304" y="180" width="80" height="4" rx="2" fill="#6A717D"/>
|
||||
<path d="M448.284 268.284a39.997 39.997 0 00-10.054-63.888 39.997 39.997 0 00-24.383-3.92l2.461 15.81a24 24 0 0120.663 40.685l11.313 11.313z" fill="#54B399"/>
|
||||
<mask id="a" maskUnits="userSpaceOnUse" x="385" y="250" width="72" height="31" fill="#000">
|
||||
<path fill="#fff" d="M385 250h72v31h-72z"/>
|
||||
<path d="M386.416 261.728a39.996 39.996 0 0034.591 18.259 39.994 39.994 0 0033.628-19.976l-13.854-8.005a24 24 0 01-40.932 1.031l-13.433 8.691z"/>
|
||||
</mask>
|
||||
<path d="M386.416 261.728a39.996 39.996 0 0034.591 18.259 39.994 39.994 0 0033.628-19.976l-13.854-8.005a24 24 0 01-40.932 1.031l-13.433 8.691z" fill="#6092C0"/>
|
||||
<path d="M386.416 261.728a39.996 39.996 0 0034.591 18.259 39.994 39.994 0 0033.628-19.976l-13.854-8.005a24 24 0 01-40.932 1.031l-13.433 8.691z" stroke="#fff" stroke-width="2" mask="url(#a)"/>
|
||||
<mask id="b" maskUnits="userSpaceOnUse" x="379" y="213" width="24" height="54" fill="#000">
|
||||
<path fill="#fff" d="M379 213h24v54h-24z"/>
|
||||
<path d="M388.645 215.163a40 40 0 00.54 50.341l12.326-10.202a24 24 0 01-.324-30.204l-12.542-9.935z"/>
|
||||
</mask>
|
||||
<path d="M388.645 215.163a40 40 0 00.54 50.341l12.326-10.202a24 24 0 01-.324-30.204l-12.542-9.935z" fill="#D36086"/>
|
||||
<path d="M388.645 215.163a40 40 0 00.54 50.341l12.326-10.202a24 24 0 01-.324-30.204l-12.542-9.935z" stroke="#fff" stroke-width="2" mask="url(#b)"/>
|
||||
<mask id="c" maskUnits="userSpaceOnUse" x="384" y="199" width="37" height="30" fill="#000">
|
||||
<path fill="#fff" d="M384 199h37v30h-37z"/>
|
||||
<path d="M420 200a40.005 40.005 0 00-33.929 18.814l13.572 8.474A24.004 24.004 0 01420 216v-16z"/>
|
||||
</mask>
|
||||
<path d="M420 200a40.005 40.005 0 00-33.929 18.814l13.572 8.474A24.004 24.004 0 01420 216v-16z" fill="#9170B8"/>
|
||||
<path d="M420 200a40.005 40.005 0 00-33.929 18.814l13.572 8.474A24.004 24.004 0 01420 216v-16z" stroke="#fff" stroke-width="2" mask="url(#c)"/>
|
||||
<rect x="367" y="192" width="22" height="4" rx="2" fill="#98A2B3"/>
|
||||
<rect x="345" y="250" width="16" height="4" rx="2" fill="#98A2B3"/>
|
||||
<rect x="441" y="284" width="24" height="4" rx="2" fill="#98A2B3"/>
|
||||
<rect x="471" y="210" width="24" height="4" rx="2" fill="#98A2B3"/>
|
||||
<path d="M393.5 194h4l4 8.5m65 9.5h-4l-7 4.5m-26.5 64l3.5 5.5h4m-58-41l-9 7h-4" stroke="#D3DAE6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<defs>
|
||||
<filter id="kibana_dashboard_light__filter0_d" x="8" y="160" width="280" height="156" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dy="4"/>
|
||||
<feGaussianBlur stdDeviation="8"/>
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0"/>
|
||||
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feBlend in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<filter id="kibana_dashboard_light__filter1_d" x="8" y="12" width="552" height="156" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dy="4"/>
|
||||
<feGaussianBlur stdDeviation="8"/>
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0"/>
|
||||
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feBlend in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
<filter id="kibana_dashboard_light__filter2_d" x="280" y="160" width="280" height="156" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
||||
<feFlood flood-opacity="0" result="BackgroundImageFix"/>
|
||||
<feColorMatrix in="SourceAlpha" values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 127 0"/>
|
||||
<feOffset dy="4"/>
|
||||
<feGaussianBlur stdDeviation="8"/>
|
||||
<feColorMatrix values="0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0.05 0"/>
|
||||
<feBlend in2="BackgroundImageFix" result="effect1_dropShadow"/>
|
||||
<feBlend in="SourceGraphic" in2="effect1_dropShadow" result="shape"/>
|
||||
</filter>
|
||||
</defs>
|
||||
</svg>
|
After Width: | Height: | Size: 9.1 KiB |
|
@ -59,13 +59,13 @@ export function Header({ color, datePicker = null, restrictWidth }: Props) {
|
|||
</HeaderMenuPortal>
|
||||
<Wrapper restrictWidth={restrictWidth}>
|
||||
<EuiSpacer size="l" />
|
||||
<EuiFlexGroup justifyContent="spaceBetween">
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiIcon type="logoObservability" size="xxl" data-test-subj="observability-logo" />
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem>
|
||||
<EuiFlexItem grow={false} style={{ alignSelf: 'center' }}>
|
||||
<EuiTitle>
|
||||
<h1>
|
||||
{i18n.translate('xpack.observability.home.title', {
|
||||
|
|
|
@ -0,0 +1,32 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiImage } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
|
||||
|
||||
export function EmptyView() {
|
||||
const {
|
||||
services: { http },
|
||||
} = useKibana();
|
||||
|
||||
return (
|
||||
<Wrapper>
|
||||
<EuiImage
|
||||
alt="Visulization"
|
||||
url={http!.basePath.prepend(`/plugins/observability/assets/kibana_dashboard_light.svg`)}
|
||||
/>
|
||||
</Wrapper>
|
||||
);
|
||||
}
|
||||
|
||||
const Wrapper = styled.div`
|
||||
text-align: center;
|
||||
opacity: 0.4;
|
||||
height: 550px;
|
||||
`;
|
|
@ -0,0 +1,138 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react';
|
||||
import { mockIndexPattern, render } from '../rtl_helpers';
|
||||
import { buildFilterLabel, FilterLabel } from './filter_label';
|
||||
import * as useSeriesHook from '../hooks/use_series_filters';
|
||||
|
||||
describe('FilterLabel', function () {
|
||||
const invertFilter = jest.fn();
|
||||
jest.spyOn(useSeriesHook, 'useSeriesFilters').mockReturnValue({
|
||||
invertFilter,
|
||||
} as any);
|
||||
|
||||
it('should render properly', async function () {
|
||||
render(
|
||||
<FilterLabel
|
||||
field={'service.name'}
|
||||
value={'elastic-co'}
|
||||
label={'Web Application'}
|
||||
negate={false}
|
||||
seriesId={'kpi-trends'}
|
||||
removeFilter={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
screen.getByText('elastic-co');
|
||||
screen.getByText(/web application:/i);
|
||||
screen.getByTitle('Delete Web Application: elastic-co');
|
||||
screen.getByRole('button', {
|
||||
name: /delete web application: elastic-co/i,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it.skip('should delete filter', async function () {
|
||||
const removeFilter = jest.fn();
|
||||
render(
|
||||
<FilterLabel
|
||||
field={'service.name'}
|
||||
value={'elastic-co'}
|
||||
label={'Web Application'}
|
||||
negate={false}
|
||||
seriesId={'kpi-trends'}
|
||||
removeFilter={removeFilter}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByLabelText('Filter actions'));
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByTestId('deleteFilter'));
|
||||
expect(removeFilter).toHaveBeenCalledTimes(1);
|
||||
expect(removeFilter).toHaveBeenCalledWith('service.name', 'elastic-co', false);
|
||||
});
|
||||
|
||||
it.skip('should invert filter', async function () {
|
||||
const removeFilter = jest.fn();
|
||||
render(
|
||||
<FilterLabel
|
||||
field={'service.name'}
|
||||
value={'elastic-co'}
|
||||
label={'Web Application'}
|
||||
negate={false}
|
||||
seriesId={'kpi-trends'}
|
||||
removeFilter={removeFilter}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
fireEvent.click(screen.getByLabelText('Filter actions'));
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByTestId('negateFilter'));
|
||||
expect(invertFilter).toHaveBeenCalledTimes(1);
|
||||
expect(invertFilter).toHaveBeenCalledWith({
|
||||
field: 'service.name',
|
||||
negate: false,
|
||||
value: 'elastic-co',
|
||||
});
|
||||
});
|
||||
|
||||
it('should display invert filter', async function () {
|
||||
render(
|
||||
<FilterLabel
|
||||
field={'service.name'}
|
||||
value={'elastic-co'}
|
||||
label={'Web Application'}
|
||||
negate={true}
|
||||
seriesId={'kpi-trends'}
|
||||
removeFilter={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
await waitFor(() => {
|
||||
screen.getByText('elastic-co');
|
||||
screen.getByText(/web application:/i);
|
||||
screen.getByTitle('Delete NOT Web Application: elastic-co');
|
||||
screen.getByRole('button', {
|
||||
name: /delete not web application: elastic-co/i,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should build filter meta', function () {
|
||||
expect(
|
||||
buildFilterLabel({
|
||||
field: 'user_agent.name',
|
||||
label: 'Browser family',
|
||||
indexPattern: mockIndexPattern,
|
||||
value: 'Firefox',
|
||||
negate: false,
|
||||
})
|
||||
).toEqual({
|
||||
meta: {
|
||||
alias: null,
|
||||
disabled: false,
|
||||
index: 'apm-*',
|
||||
key: 'Browser family',
|
||||
negate: false,
|
||||
type: 'phrase',
|
||||
value: 'Firefox',
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'user_agent.name': 'Firefox',
|
||||
},
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,90 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { injectI18n } from '@kbn/i18n/react';
|
||||
import { esFilters, Filter, IndexPattern } from '../../../../../../../../src/plugins/data/public';
|
||||
import { useIndexPatternContext } from '../hooks/use_default_index_pattern';
|
||||
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
|
||||
import { useSeriesFilters } from '../hooks/use_series_filters';
|
||||
|
||||
interface Props {
|
||||
field: string;
|
||||
label: string;
|
||||
value: string;
|
||||
seriesId: string;
|
||||
negate: boolean;
|
||||
definitionFilter?: boolean;
|
||||
removeFilter: (field: string, value: string, notVal: boolean) => void;
|
||||
}
|
||||
export function buildFilterLabel({
|
||||
field,
|
||||
value,
|
||||
label,
|
||||
indexPattern,
|
||||
negate,
|
||||
}: {
|
||||
label: string;
|
||||
value: string;
|
||||
negate: boolean;
|
||||
field: string;
|
||||
indexPattern: IndexPattern;
|
||||
}) {
|
||||
const indexField = indexPattern.getFieldByName(field)!;
|
||||
|
||||
const filter = esFilters.buildPhraseFilter(indexField, value, indexPattern);
|
||||
|
||||
filter.meta.value = value;
|
||||
filter.meta.key = label;
|
||||
filter.meta.alias = null;
|
||||
filter.meta.negate = negate;
|
||||
filter.meta.disabled = false;
|
||||
filter.meta.type = 'phrase';
|
||||
|
||||
return filter;
|
||||
}
|
||||
export function FilterLabel({
|
||||
label,
|
||||
seriesId,
|
||||
field,
|
||||
value,
|
||||
negate,
|
||||
removeFilter,
|
||||
definitionFilter,
|
||||
}: Props) {
|
||||
const FilterItem = injectI18n(esFilters.FilterItem);
|
||||
|
||||
const { indexPattern } = useIndexPatternContext();
|
||||
|
||||
const filter = buildFilterLabel({ field, value, label, indexPattern, negate });
|
||||
|
||||
const { invertFilter } = useSeriesFilters({ seriesId });
|
||||
|
||||
const {
|
||||
services: { uiSettings },
|
||||
} = useKibana();
|
||||
|
||||
return indexPattern ? (
|
||||
<FilterItem
|
||||
indexPatterns={[indexPattern]}
|
||||
id={`${field}-${value}-${negate}`}
|
||||
filter={filter}
|
||||
onRemove={() => {
|
||||
removeFilter(field, value, false);
|
||||
}}
|
||||
onUpdate={(filterN: Filter) => {
|
||||
if (definitionFilter) {
|
||||
// FIXME handle this use case
|
||||
} else if (filterN.meta.negate !== negate) {
|
||||
invertFilter({ field, value, negate });
|
||||
}
|
||||
}}
|
||||
uiSettings={uiSettings!}
|
||||
hiddenPanelOptions={['pinFilter', 'editFilter', 'disableFilter']}
|
||||
/>
|
||||
) : null;
|
||||
}
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { AppDataType, ReportViewTypeId } from '../types';
|
||||
import {
|
||||
CLS_FIELD,
|
||||
FCP_FIELD,
|
||||
FID_FIELD,
|
||||
LCP_FIELD,
|
||||
TBT_FIELD,
|
||||
} from './data/elasticsearch_fieldnames';
|
||||
|
||||
export const FieldLabels: Record<string, string> = {
|
||||
'user_agent.name': 'Browser family',
|
||||
'user_agent.version': 'Browser version',
|
||||
'user_agent.os.name': 'Operating system',
|
||||
'client.geo.country_name': 'Location',
|
||||
'user_agent.device.name': 'Device',
|
||||
'observer.geo.name': 'Observer location',
|
||||
'service.name': 'Service Name',
|
||||
'service.environment': 'Environment',
|
||||
|
||||
[LCP_FIELD]: 'Largest contentful paint',
|
||||
[FCP_FIELD]: 'First contentful paint',
|
||||
[TBT_FIELD]: 'Total blocking time',
|
||||
[FID_FIELD]: 'First input delay',
|
||||
[CLS_FIELD]: 'Cumulative layout shift',
|
||||
|
||||
'monitor.id': 'Monitor Id',
|
||||
'monitor.status': 'Monitor Status',
|
||||
|
||||
'agent.hostname': 'Agent host',
|
||||
'host.hostname': 'Host name',
|
||||
'monitor.name': 'Monitor name',
|
||||
'monitor.type': 'Monitor Type',
|
||||
'url.port': 'Port',
|
||||
tags: 'Tags',
|
||||
|
||||
// custom
|
||||
|
||||
'performance.metric': 'Metric',
|
||||
'Business.KPI': 'KPI',
|
||||
};
|
||||
|
||||
export const DataViewLabels: Record<ReportViewTypeId, string> = {
|
||||
pld: 'Performance Distribution',
|
||||
upd: 'Uptime monitor duration',
|
||||
upp: 'Uptime pings',
|
||||
svl: 'APM Service latency',
|
||||
kpi: 'KPI over time',
|
||||
tpt: 'APM Service throughput',
|
||||
cpu: 'System CPU Usage',
|
||||
logs: 'Logs Frequency',
|
||||
mem: 'System Memory Usage',
|
||||
nwk: 'Network Activity',
|
||||
};
|
||||
|
||||
export const ReportToDataTypeMap: Record<ReportViewTypeId, AppDataType> = {
|
||||
upd: 'synthetics',
|
||||
upp: 'synthetics',
|
||||
tpt: 'apm',
|
||||
svl: 'apm',
|
||||
kpi: 'rum',
|
||||
pld: 'rum',
|
||||
nwk: 'metrics',
|
||||
mem: 'metrics',
|
||||
logs: 'logs',
|
||||
cpu: 'metrics',
|
||||
};
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DataSeries } from '../types';
|
||||
import { FieldLabels } from './constants';
|
||||
import { OperationType } from '../../../../../../lens/public';
|
||||
|
||||
interface Props {
|
||||
seriesId: string;
|
||||
}
|
||||
|
||||
export function getCPUUsageLensConfig({ seriesId }: Props): DataSeries {
|
||||
return {
|
||||
id: seriesId,
|
||||
reportType: 'cpu-usage',
|
||||
defaultSeriesType: 'line',
|
||||
seriesTypes: ['line', 'bar'],
|
||||
xAxisColumn: {
|
||||
sourceField: '@timestamp',
|
||||
},
|
||||
yAxisColumn: {
|
||||
operationType: 'avg' as OperationType,
|
||||
sourceField: 'system.cpu.user.pct',
|
||||
label: 'CPU Usage %',
|
||||
},
|
||||
hasMetricType: true,
|
||||
defaultFilters: [],
|
||||
breakdowns: ['host.hostname'],
|
||||
filters: [],
|
||||
labels: { ...FieldLabels, 'host.hostname': 'Host name' },
|
||||
reportDefinitions: [
|
||||
{
|
||||
field: 'agent.hostname',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
|
@ -0,0 +1,144 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export const CLOUD = 'cloud';
|
||||
export const CLOUD_AVAILABILITY_ZONE = 'cloud.availability_zone';
|
||||
export const CLOUD_PROVIDER = 'cloud.provider';
|
||||
export const CLOUD_REGION = 'cloud.region';
|
||||
export const CLOUD_MACHINE_TYPE = 'cloud.machine.type';
|
||||
|
||||
export const SERVICE = 'service';
|
||||
export const SERVICE_NAME = 'service.name';
|
||||
export const SERVICE_ENVIRONMENT = 'service.environment';
|
||||
export const SERVICE_FRAMEWORK_NAME = 'service.framework.name';
|
||||
export const SERVICE_FRAMEWORK_VERSION = 'service.framework.version';
|
||||
export const SERVICE_LANGUAGE_NAME = 'service.language.name';
|
||||
export const SERVICE_LANGUAGE_VERSION = 'service.language.version';
|
||||
export const SERVICE_RUNTIME_NAME = 'service.runtime.name';
|
||||
export const SERVICE_RUNTIME_VERSION = 'service.runtime.version';
|
||||
export const SERVICE_NODE_NAME = 'service.node.name';
|
||||
export const SERVICE_VERSION = 'service.version';
|
||||
|
||||
export const AGENT = 'agent';
|
||||
export const AGENT_NAME = 'agent.name';
|
||||
export const AGENT_VERSION = 'agent.version';
|
||||
|
||||
export const URL_FULL = 'url.full';
|
||||
export const HTTP_REQUEST_METHOD = 'http.request.method';
|
||||
export const HTTP_RESPONSE_STATUS_CODE = 'http.response.status_code';
|
||||
export const USER_ID = 'user.id';
|
||||
export const USER_AGENT_ORIGINAL = 'user_agent.original';
|
||||
export const USER_AGENT_NAME = 'user_agent.name';
|
||||
export const USER_AGENT_VERSION = 'user_agent.version';
|
||||
|
||||
export const DESTINATION_ADDRESS = 'destination.address';
|
||||
|
||||
export const OBSERVER_HOSTNAME = 'observer.hostname';
|
||||
export const OBSERVER_VERSION_MAJOR = 'observer.version_major';
|
||||
export const OBSERVER_LISTENING = 'observer.listening';
|
||||
export const PROCESSOR_EVENT = 'processor.event';
|
||||
|
||||
export const TRANSACTION_DURATION = 'transaction.duration.us';
|
||||
export const TRANSACTION_DURATION_HISTOGRAM = 'transaction.duration.histogram';
|
||||
export const TRANSACTION_TYPE = 'transaction.type';
|
||||
export const TRANSACTION_RESULT = 'transaction.result';
|
||||
export const TRANSACTION_NAME = 'transaction.name';
|
||||
export const TRANSACTION_ID = 'transaction.id';
|
||||
export const TRANSACTION_SAMPLED = 'transaction.sampled';
|
||||
export const TRANSACTION_BREAKDOWN_COUNT = 'transaction.breakdown.count';
|
||||
export const TRANSACTION_PAGE_URL = 'transaction.page.url';
|
||||
// for transaction metrics
|
||||
export const TRANSACTION_ROOT = 'transaction.root';
|
||||
|
||||
export const EVENT_OUTCOME = 'event.outcome';
|
||||
|
||||
export const TRACE_ID = 'trace.id';
|
||||
|
||||
export const SPAN_DURATION = 'span.duration.us';
|
||||
export const SPAN_TYPE = 'span.type';
|
||||
export const SPAN_SUBTYPE = 'span.subtype';
|
||||
export const SPAN_SELF_TIME_SUM = 'span.self_time.sum.us';
|
||||
export const SPAN_ACTION = 'span.action';
|
||||
export const SPAN_NAME = 'span.name';
|
||||
export const SPAN_ID = 'span.id';
|
||||
export const SPAN_DESTINATION_SERVICE_RESOURCE = 'span.destination.service.resource';
|
||||
export const SPAN_DESTINATION_SERVICE_RESPONSE_TIME_COUNT =
|
||||
'span.destination.service.response_time.count';
|
||||
|
||||
export const SPAN_DESTINATION_SERVICE_RESPONSE_TIME_SUM =
|
||||
'span.destination.service.response_time.sum.us';
|
||||
|
||||
// Parent ID for a transaction or span
|
||||
export const PARENT_ID = 'parent.id';
|
||||
|
||||
export const ERROR_GROUP_ID = 'error.grouping_key';
|
||||
export const ERROR_CULPRIT = 'error.culprit';
|
||||
export const ERROR_LOG_LEVEL = 'error.log.level';
|
||||
export const ERROR_LOG_MESSAGE = 'error.log.message';
|
||||
export const ERROR_EXC_MESSAGE = 'error.exception.message'; // only to be used in es queries, since error.exception is now an array
|
||||
export const ERROR_EXC_HANDLED = 'error.exception.handled'; // only to be used in es queries, since error.exception is now an array
|
||||
export const ERROR_EXC_TYPE = 'error.exception.type';
|
||||
export const ERROR_PAGE_URL = 'error.page.url';
|
||||
|
||||
// METRICS
|
||||
export const METRIC_SYSTEM_FREE_MEMORY = 'system.memory.actual.free';
|
||||
export const METRIC_SYSTEM_TOTAL_MEMORY = 'system.memory.total';
|
||||
export const METRIC_SYSTEM_CPU_PERCENT = 'system.cpu.total.norm.pct';
|
||||
export const METRIC_PROCESS_CPU_PERCENT = 'system.process.cpu.total.norm.pct';
|
||||
export const METRIC_CGROUP_MEMORY_LIMIT_BYTES = 'system.process.cgroup.memory.mem.limit.bytes';
|
||||
export const METRIC_CGROUP_MEMORY_USAGE_BYTES = 'system.process.cgroup.memory.mem.usage.bytes';
|
||||
|
||||
export const METRIC_JAVA_HEAP_MEMORY_MAX = 'jvm.memory.heap.max';
|
||||
export const METRIC_JAVA_HEAP_MEMORY_COMMITTED = 'jvm.memory.heap.committed';
|
||||
export const METRIC_JAVA_HEAP_MEMORY_USED = 'jvm.memory.heap.used';
|
||||
export const METRIC_JAVA_NON_HEAP_MEMORY_MAX = 'jvm.memory.non_heap.max';
|
||||
export const METRIC_JAVA_NON_HEAP_MEMORY_COMMITTED = 'jvm.memory.non_heap.committed';
|
||||
export const METRIC_JAVA_NON_HEAP_MEMORY_USED = 'jvm.memory.non_heap.used';
|
||||
export const METRIC_JAVA_THREAD_COUNT = 'jvm.thread.count';
|
||||
export const METRIC_JAVA_GC_COUNT = 'jvm.gc.count';
|
||||
export const METRIC_JAVA_GC_TIME = 'jvm.gc.time';
|
||||
|
||||
export const LABEL_NAME = 'labels.name';
|
||||
|
||||
export const HOST = 'host';
|
||||
export const HOST_NAME = 'host.hostname';
|
||||
export const HOST_OS_PLATFORM = 'host.os.platform';
|
||||
export const CONTAINER_ID = 'container.id';
|
||||
export const KUBERNETES = 'kubernetes';
|
||||
export const POD_NAME = 'kubernetes.pod.name';
|
||||
|
||||
export const CLIENT_GEO_COUNTRY_ISO_CODE = 'client.geo.country_iso_code';
|
||||
export const CLIENT_GEO_COUNTRY_NAME = 'client.geo.country_name';
|
||||
|
||||
// RUM Labels
|
||||
export const TRANSACTION_URL = 'url.full';
|
||||
export const CLIENT_GEO = 'client.geo';
|
||||
export const USER_AGENT_DEVICE = 'user_agent.device.name';
|
||||
export const USER_AGENT_OS = 'user_agent.os.name';
|
||||
|
||||
export const TRANSACTION_TIME_TO_FIRST_BYTE = 'transaction.marks.agent.timeToFirstByte';
|
||||
export const TRANSACTION_DOM_INTERACTIVE = 'transaction.marks.agent.domInteractive';
|
||||
|
||||
export const FCP_FIELD = 'transaction.marks.agent.firstContentfulPaint';
|
||||
export const LCP_FIELD = 'transaction.marks.agent.largestContentfulPaint';
|
||||
export const TBT_FIELD = 'transaction.experience.tbt';
|
||||
export const FID_FIELD = 'transaction.experience.fid';
|
||||
export const CLS_FIELD = 'transaction.experience.cls';
|
||||
|
||||
export const PROFILE_ID = 'profile.id';
|
||||
export const PROFILE_DURATION = 'profile.duration';
|
||||
export const PROFILE_TOP_ID = 'profile.top.id';
|
||||
export const PROFILE_STACK = 'profile.stack';
|
||||
|
||||
export const PROFILE_SAMPLES_COUNT = 'profile.samples.count';
|
||||
export const PROFILE_CPU_NS = 'profile.cpu.ns';
|
||||
export const PROFILE_WALL_US = 'profile.wall.us';
|
||||
|
||||
export const PROFILE_ALLOC_OBJECTS = 'profile.alloc_objects.count';
|
||||
export const PROFILE_ALLOC_SPACE = 'profile.alloc_space.bytes';
|
||||
export const PROFILE_INUSE_OBJECTS = 'profile.inuse_objects.count';
|
||||
export const PROFILE_INUSE_SPACE = 'profile.inuse_space.bytes';
|
|
@ -0,0 +1,74 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
export const sampleAttribute = {
|
||||
title: 'Prefilled from exploratory view app',
|
||||
description: '',
|
||||
visualizationType: 'lnsXY',
|
||||
references: [
|
||||
{ id: 'apm-*', name: 'indexpattern-datasource-current-indexpattern', type: 'index-pattern' },
|
||||
{ id: 'apm-*', name: 'indexpattern-datasource-layer-layer1', type: 'index-pattern' },
|
||||
],
|
||||
state: {
|
||||
datasourceStates: {
|
||||
indexpattern: {
|
||||
layers: {
|
||||
layer1: {
|
||||
columnOrder: ['x-axis-column', 'y-axis-column'],
|
||||
columns: {
|
||||
'x-axis-column': {
|
||||
sourceField: 'transaction.duration.us',
|
||||
label: 'Page load time',
|
||||
dataType: 'number',
|
||||
operationType: 'range',
|
||||
isBucketed: true,
|
||||
scale: 'interval',
|
||||
params: {
|
||||
type: 'histogram',
|
||||
ranges: [{ from: 0, to: 1000, label: '' }],
|
||||
maxBars: 'auto',
|
||||
},
|
||||
},
|
||||
'y-axis-column': {
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
label: 'Pages loaded',
|
||||
operationType: 'count',
|
||||
scale: 'ratio',
|
||||
sourceField: 'Records',
|
||||
},
|
||||
},
|
||||
incompleteColumns: {},
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
visualization: {
|
||||
legend: { isVisible: true, position: 'right' },
|
||||
valueLabels: 'hide',
|
||||
fittingFunction: 'Linear',
|
||||
curveType: 'CURVE_MONOTONE_X',
|
||||
axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true },
|
||||
tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true },
|
||||
gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true },
|
||||
preferredSeriesType: 'line',
|
||||
layers: [
|
||||
{
|
||||
accessors: ['y-axis-column'],
|
||||
layerId: 'layer1',
|
||||
seriesType: 'line',
|
||||
yConfig: [{ forAccessor: 'y-axis-column', color: 'green' }],
|
||||
xAccessor: 'x-axis-column',
|
||||
},
|
||||
],
|
||||
},
|
||||
query: { query: '', language: 'kuery' },
|
||||
filters: [
|
||||
{ meta: { index: 'apm-*' }, query: { match_phrase: { 'transaction.type': 'page-load' } } },
|
||||
{ meta: { index: 'apm-*' }, query: { match_phrase: { 'processor.event': 'transaction' } } },
|
||||
],
|
||||
},
|
||||
};
|
File diff suppressed because one or more lines are too long
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ReportViewTypes } from '../types';
|
||||
import { getPerformanceDistLensConfig } from './performance_dist_config';
|
||||
import { getMonitorDurationConfig } from './monitor_duration_config';
|
||||
import { getServiceLatencyLensConfig } from './service_latency_config';
|
||||
import { getMonitorPingsConfig } from './monitor_pings_config';
|
||||
import { getServiceThroughputLensConfig } from './service_throughput_config';
|
||||
import { getKPITrendsLensConfig } from './kpi_trends_config';
|
||||
import { getCPUUsageLensConfig } from './cpu_usage_config';
|
||||
import { getMemoryUsageLensConfig } from './memory_usage_config';
|
||||
import { getNetworkActivityLensConfig } from './network_activity_config';
|
||||
import { getLogsFrequencyLensConfig } from './logs_frequency_config';
|
||||
import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns';
|
||||
|
||||
interface Props {
|
||||
reportType: keyof typeof ReportViewTypes;
|
||||
seriesId: string;
|
||||
indexPattern: IIndexPattern;
|
||||
}
|
||||
|
||||
export const getDefaultConfigs = ({ reportType, seriesId, indexPattern }: Props) => {
|
||||
switch (ReportViewTypes[reportType]) {
|
||||
case 'page-load-dist':
|
||||
return getPerformanceDistLensConfig({ seriesId, indexPattern });
|
||||
case 'kpi-trends':
|
||||
return getKPITrendsLensConfig({ seriesId, indexPattern });
|
||||
case 'uptime-duration':
|
||||
return getMonitorDurationConfig({ seriesId });
|
||||
case 'uptime-pings':
|
||||
return getMonitorPingsConfig({ seriesId });
|
||||
case 'service-latency':
|
||||
return getServiceLatencyLensConfig({ seriesId, indexPattern });
|
||||
case 'service-throughput':
|
||||
return getServiceThroughputLensConfig({ seriesId, indexPattern });
|
||||
case 'cpu-usage':
|
||||
return getCPUUsageLensConfig({ seriesId });
|
||||
case 'memory-usage':
|
||||
return getMemoryUsageLensConfig({ seriesId });
|
||||
case 'network-activity':
|
||||
return getNetworkActivityLensConfig({ seriesId });
|
||||
case 'logs-frequency':
|
||||
return getLogsFrequencyLensConfig({ seriesId });
|
||||
default:
|
||||
return getKPITrendsLensConfig({ seriesId, indexPattern });
|
||||
}
|
||||
};
|
|
@ -0,0 +1,73 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ConfigProps, DataSeries } from '../types';
|
||||
import { FieldLabels } from './constants';
|
||||
import { buildPhraseFilter } from './utils';
|
||||
import {
|
||||
CLIENT_GEO_COUNTRY_NAME,
|
||||
PROCESSOR_EVENT,
|
||||
SERVICE_ENVIRONMENT,
|
||||
SERVICE_NAME,
|
||||
TRANSACTION_TYPE,
|
||||
USER_AGENT_DEVICE,
|
||||
USER_AGENT_NAME,
|
||||
USER_AGENT_OS,
|
||||
USER_AGENT_VERSION,
|
||||
} from './data/elasticsearch_fieldnames';
|
||||
|
||||
export function getKPITrendsLensConfig({ seriesId, indexPattern }: ConfigProps): DataSeries {
|
||||
return {
|
||||
id: seriesId,
|
||||
defaultSeriesType: 'bar_stacked',
|
||||
reportType: 'kpi-trends',
|
||||
seriesTypes: ['bar', 'bar_stacked'],
|
||||
xAxisColumn: {
|
||||
sourceField: '@timestamp',
|
||||
},
|
||||
yAxisColumn: {
|
||||
operationType: 'count',
|
||||
label: 'Page views',
|
||||
},
|
||||
hasMetricType: false,
|
||||
defaultFilters: [
|
||||
USER_AGENT_OS,
|
||||
CLIENT_GEO_COUNTRY_NAME,
|
||||
USER_AGENT_DEVICE,
|
||||
{
|
||||
field: USER_AGENT_NAME,
|
||||
nested: USER_AGENT_VERSION,
|
||||
},
|
||||
],
|
||||
breakdowns: [USER_AGENT_NAME, USER_AGENT_OS, CLIENT_GEO_COUNTRY_NAME, USER_AGENT_DEVICE],
|
||||
filters: [
|
||||
buildPhraseFilter(TRANSACTION_TYPE, 'page-load', indexPattern),
|
||||
buildPhraseFilter(PROCESSOR_EVENT, 'transaction', indexPattern),
|
||||
],
|
||||
labels: { ...FieldLabels, SERVICE_NAME: 'Web Application' },
|
||||
reportDefinitions: [
|
||||
{
|
||||
field: SERVICE_NAME,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
field: SERVICE_ENVIRONMENT,
|
||||
},
|
||||
{
|
||||
field: 'Business.KPI',
|
||||
custom: true,
|
||||
defaultValue: 'Records',
|
||||
options: [
|
||||
{
|
||||
field: 'Records',
|
||||
label: 'Page views',
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
|
@ -0,0 +1,387 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { LensAttributes } from './lens_attributes';
|
||||
import { mockIndexPattern } from '../rtl_helpers';
|
||||
import { getDefaultConfigs } from './default_configs';
|
||||
import { sampleAttribute } from './data/sample_attribute';
|
||||
import { LCP_FIELD, SERVICE_NAME } from './data/elasticsearch_fieldnames';
|
||||
import { USER_AGENT_NAME } from './data/elasticsearch_fieldnames';
|
||||
|
||||
describe('Lens Attribute', () => {
|
||||
const reportViewConfig = getDefaultConfigs({
|
||||
reportType: 'pld',
|
||||
indexPattern: mockIndexPattern,
|
||||
seriesId: 'series-id',
|
||||
});
|
||||
|
||||
let lnsAttr: LensAttributes;
|
||||
|
||||
beforeEach(() => {
|
||||
lnsAttr = new LensAttributes(mockIndexPattern, reportViewConfig, 'line', [], 'count', {});
|
||||
});
|
||||
|
||||
it('should return expected json', function () {
|
||||
expect(lnsAttr.getJSON()).toEqual(sampleAttribute);
|
||||
});
|
||||
|
||||
it('should return main y axis', function () {
|
||||
expect(lnsAttr.getMainYAxis()).toEqual({
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
label: 'Pages loaded',
|
||||
operationType: 'count',
|
||||
scale: 'ratio',
|
||||
sourceField: 'Records',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return expected field type', function () {
|
||||
expect(JSON.stringify(lnsAttr.getFieldMeta('transaction.type'))).toEqual(
|
||||
JSON.stringify({
|
||||
count: 0,
|
||||
name: 'transaction.type',
|
||||
type: 'string',
|
||||
esTypes: ['keyword'],
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
readFromDocValues: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should return expected field type for custom field with default value', function () {
|
||||
expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric'))).toEqual(
|
||||
JSON.stringify({
|
||||
count: 0,
|
||||
name: 'transaction.duration.us',
|
||||
type: 'number',
|
||||
esTypes: ['long'],
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
readFromDocValues: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should return expected field type for custom field with passed value', function () {
|
||||
lnsAttr = new LensAttributes(mockIndexPattern, reportViewConfig, 'line', [], 'count', {
|
||||
'performance.metric': LCP_FIELD,
|
||||
});
|
||||
|
||||
expect(JSON.stringify(lnsAttr.getFieldMeta('performance.metric'))).toEqual(
|
||||
JSON.stringify({
|
||||
count: 0,
|
||||
name: LCP_FIELD,
|
||||
type: 'number',
|
||||
esTypes: ['scaled_float'],
|
||||
scripted: false,
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
readFromDocValues: true,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should return expected number column', function () {
|
||||
expect(lnsAttr.getNumberColumn('transaction.duration.us')).toEqual({
|
||||
dataType: 'number',
|
||||
isBucketed: true,
|
||||
label: 'Page load time',
|
||||
operationType: 'range',
|
||||
params: {
|
||||
maxBars: 'auto',
|
||||
ranges: [
|
||||
{
|
||||
from: 0,
|
||||
label: '',
|
||||
to: 1000,
|
||||
},
|
||||
],
|
||||
type: 'histogram',
|
||||
},
|
||||
scale: 'interval',
|
||||
sourceField: 'transaction.duration.us',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return expected date histogram column', function () {
|
||||
expect(lnsAttr.getDateHistogramColumn('@timestamp')).toEqual({
|
||||
dataType: 'date',
|
||||
isBucketed: true,
|
||||
label: '@timestamp',
|
||||
operationType: 'date_histogram',
|
||||
params: {
|
||||
interval: 'auto',
|
||||
},
|
||||
scale: 'interval',
|
||||
sourceField: '@timestamp',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return main x axis', function () {
|
||||
expect(lnsAttr.getXAxis()).toEqual({
|
||||
dataType: 'number',
|
||||
isBucketed: true,
|
||||
label: 'Page load time',
|
||||
operationType: 'range',
|
||||
params: {
|
||||
maxBars: 'auto',
|
||||
ranges: [
|
||||
{
|
||||
from: 0,
|
||||
label: '',
|
||||
to: 1000,
|
||||
},
|
||||
],
|
||||
type: 'histogram',
|
||||
},
|
||||
scale: 'interval',
|
||||
sourceField: 'transaction.duration.us',
|
||||
});
|
||||
});
|
||||
|
||||
it('should return first layer', function () {
|
||||
expect(lnsAttr.getLayer()).toEqual({
|
||||
columnOrder: ['x-axis-column', 'y-axis-column'],
|
||||
columns: {
|
||||
'x-axis-column': {
|
||||
dataType: 'number',
|
||||
isBucketed: true,
|
||||
label: 'Page load time',
|
||||
operationType: 'range',
|
||||
params: {
|
||||
maxBars: 'auto',
|
||||
ranges: [
|
||||
{
|
||||
from: 0,
|
||||
label: '',
|
||||
to: 1000,
|
||||
},
|
||||
],
|
||||
type: 'histogram',
|
||||
},
|
||||
scale: 'interval',
|
||||
sourceField: 'transaction.duration.us',
|
||||
},
|
||||
'y-axis-column': {
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
label: 'Pages loaded',
|
||||
operationType: 'count',
|
||||
scale: 'ratio',
|
||||
sourceField: 'Records',
|
||||
},
|
||||
},
|
||||
incompleteColumns: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should return expected XYState', function () {
|
||||
expect(lnsAttr.getXyState()).toEqual({
|
||||
axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true },
|
||||
curveType: 'CURVE_MONOTONE_X',
|
||||
fittingFunction: 'Linear',
|
||||
gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true },
|
||||
layers: [
|
||||
{
|
||||
accessors: ['y-axis-column'],
|
||||
layerId: 'layer1',
|
||||
palette: undefined,
|
||||
seriesType: 'line',
|
||||
xAccessor: 'x-axis-column',
|
||||
yConfig: [{ color: 'green', forAccessor: 'y-axis-column' }],
|
||||
},
|
||||
],
|
||||
legend: { isVisible: true, position: 'right' },
|
||||
preferredSeriesType: 'line',
|
||||
tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true },
|
||||
valueLabels: 'hide',
|
||||
});
|
||||
});
|
||||
|
||||
describe('ParseFilters function', function () {
|
||||
it('should parse default filters', function () {
|
||||
expect(lnsAttr.parseFilters()).toEqual([
|
||||
{ meta: { index: 'apm-*' }, query: { match_phrase: { 'transaction.type': 'page-load' } } },
|
||||
{ meta: { index: 'apm-*' }, query: { match_phrase: { 'processor.event': 'transaction' } } },
|
||||
]);
|
||||
});
|
||||
|
||||
it('should parse default and ui filters', function () {
|
||||
lnsAttr = new LensAttributes(
|
||||
mockIndexPattern,
|
||||
reportViewConfig,
|
||||
'line',
|
||||
[
|
||||
{ field: SERVICE_NAME, values: ['elastic-co', 'kibana-front'] },
|
||||
{ field: USER_AGENT_NAME, values: ['Firefox'], notValues: ['Chrome'] },
|
||||
],
|
||||
'count',
|
||||
{}
|
||||
);
|
||||
|
||||
expect(lnsAttr.parseFilters()).toEqual([
|
||||
{ meta: { index: 'apm-*' }, query: { match_phrase: { 'transaction.type': 'page-load' } } },
|
||||
{ meta: { index: 'apm-*' }, query: { match_phrase: { 'processor.event': 'transaction' } } },
|
||||
{
|
||||
meta: {
|
||||
index: 'apm-*',
|
||||
key: 'service.name',
|
||||
params: ['elastic-co', 'kibana-front'],
|
||||
type: 'phrases',
|
||||
value: 'elastic-co, kibana-front',
|
||||
},
|
||||
query: {
|
||||
bool: {
|
||||
minimum_should_match: 1,
|
||||
should: [
|
||||
{
|
||||
match_phrase: {
|
||||
'service.name': 'elastic-co',
|
||||
},
|
||||
},
|
||||
{
|
||||
match_phrase: {
|
||||
'service.name': 'kibana-front',
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
index: 'apm-*',
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'user_agent.name': 'Firefox',
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
meta: {
|
||||
index: 'apm-*',
|
||||
negate: true,
|
||||
},
|
||||
query: {
|
||||
match_phrase: {
|
||||
'user_agent.name': 'Chrome',
|
||||
},
|
||||
},
|
||||
},
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Layer breakdowns', function () {
|
||||
it('should add breakdown column', function () {
|
||||
lnsAttr.addBreakdown(USER_AGENT_NAME);
|
||||
|
||||
expect(lnsAttr.visualization.layers).toEqual([
|
||||
{
|
||||
accessors: ['y-axis-column'],
|
||||
layerId: 'layer1',
|
||||
palette: undefined,
|
||||
seriesType: 'line',
|
||||
splitAccessor: 'break-down-column',
|
||||
xAccessor: 'x-axis-column',
|
||||
yConfig: [{ color: 'green', forAccessor: 'y-axis-column' }],
|
||||
},
|
||||
]);
|
||||
|
||||
expect(lnsAttr.layers.layer1).toEqual({
|
||||
columnOrder: ['x-axis-column', 'break-down-column', 'y-axis-column'],
|
||||
columns: {
|
||||
'break-down-column': {
|
||||
dataType: 'string',
|
||||
isBucketed: true,
|
||||
label: 'Top values of Browser family',
|
||||
operationType: 'terms',
|
||||
params: {
|
||||
missingBucket: false,
|
||||
orderBy: { columnId: 'y-axis-column', type: 'column' },
|
||||
orderDirection: 'desc',
|
||||
otherBucket: true,
|
||||
size: 3,
|
||||
},
|
||||
scale: 'ordinal',
|
||||
sourceField: 'user_agent.name',
|
||||
},
|
||||
'x-axis-column': {
|
||||
dataType: 'number',
|
||||
isBucketed: true,
|
||||
label: 'Page load time',
|
||||
operationType: 'range',
|
||||
params: {
|
||||
maxBars: 'auto',
|
||||
ranges: [{ from: 0, label: '', to: 1000 }],
|
||||
type: 'histogram',
|
||||
},
|
||||
scale: 'interval',
|
||||
sourceField: 'transaction.duration.us',
|
||||
},
|
||||
'y-axis-column': {
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
label: 'Pages loaded',
|
||||
operationType: 'count',
|
||||
scale: 'ratio',
|
||||
sourceField: 'Records',
|
||||
},
|
||||
},
|
||||
incompleteColumns: {},
|
||||
});
|
||||
});
|
||||
|
||||
it('should remove breakdown column', function () {
|
||||
lnsAttr.addBreakdown(USER_AGENT_NAME);
|
||||
|
||||
lnsAttr.removeBreakdown();
|
||||
|
||||
expect(lnsAttr.visualization.layers).toEqual([
|
||||
{
|
||||
accessors: ['y-axis-column'],
|
||||
layerId: 'layer1',
|
||||
palette: undefined,
|
||||
seriesType: 'line',
|
||||
xAccessor: 'x-axis-column',
|
||||
yConfig: [{ color: 'green', forAccessor: 'y-axis-column' }],
|
||||
},
|
||||
]);
|
||||
|
||||
expect(lnsAttr.layers.layer1.columnOrder).toEqual(['x-axis-column', 'y-axis-column']);
|
||||
|
||||
expect(lnsAttr.layers.layer1.columns).toEqual({
|
||||
'x-axis-column': {
|
||||
dataType: 'number',
|
||||
isBucketed: true,
|
||||
label: 'Page load time',
|
||||
operationType: 'range',
|
||||
params: {
|
||||
maxBars: 'auto',
|
||||
ranges: [{ from: 0, label: '', to: 1000 }],
|
||||
type: 'histogram',
|
||||
},
|
||||
scale: 'interval',
|
||||
sourceField: 'transaction.duration.us',
|
||||
},
|
||||
'y-axis-column': {
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
label: 'Pages loaded',
|
||||
operationType: 'count',
|
||||
scale: 'ratio',
|
||||
sourceField: 'Records',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,273 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import {
|
||||
CountIndexPatternColumn,
|
||||
DateHistogramIndexPatternColumn,
|
||||
LastValueIndexPatternColumn,
|
||||
OperationType,
|
||||
PersistedIndexPatternLayer,
|
||||
RangeIndexPatternColumn,
|
||||
SeriesType,
|
||||
TypedLensByValueInput,
|
||||
XYState,
|
||||
XYCurveType,
|
||||
DataType,
|
||||
} from '../../../../../../lens/public';
|
||||
import {
|
||||
buildPhraseFilter,
|
||||
buildPhrasesFilter,
|
||||
IndexPattern,
|
||||
} from '../../../../../../../../src/plugins/data/common';
|
||||
import { FieldLabels } from './constants';
|
||||
import { DataSeries, UrlFilter } from '../types';
|
||||
|
||||
function getLayerReferenceName(layerId: string) {
|
||||
return `indexpattern-datasource-layer-${layerId}`;
|
||||
}
|
||||
|
||||
export class LensAttributes {
|
||||
indexPattern: IndexPattern;
|
||||
layers: Record<string, PersistedIndexPatternLayer>;
|
||||
visualization: XYState;
|
||||
filters: UrlFilter[];
|
||||
seriesType: SeriesType;
|
||||
reportViewConfig: DataSeries;
|
||||
reportDefinitions: Record<string, string>;
|
||||
|
||||
constructor(
|
||||
indexPattern: IndexPattern,
|
||||
reportViewConfig: DataSeries,
|
||||
seriesType?: SeriesType,
|
||||
filters?: UrlFilter[],
|
||||
metricType?: OperationType,
|
||||
reportDefinitions?: Record<string, string>
|
||||
) {
|
||||
this.indexPattern = indexPattern;
|
||||
this.layers = {};
|
||||
this.filters = filters ?? [];
|
||||
this.reportDefinitions = reportDefinitions ?? {};
|
||||
|
||||
if (typeof reportViewConfig.yAxisColumn.operationType !== undefined && metricType) {
|
||||
reportViewConfig.yAxisColumn.operationType = metricType;
|
||||
}
|
||||
this.seriesType = seriesType ?? reportViewConfig.defaultSeriesType;
|
||||
this.reportViewConfig = reportViewConfig;
|
||||
this.layers.layer1 = this.getLayer();
|
||||
this.visualization = this.getXyState();
|
||||
}
|
||||
|
||||
addBreakdown(sourceField: string) {
|
||||
const fieldMeta = this.indexPattern.getFieldByName(sourceField);
|
||||
|
||||
this.layers.layer1.columns['break-down-column'] = {
|
||||
sourceField,
|
||||
label: `Top values of ${FieldLabels[sourceField]}`,
|
||||
dataType: fieldMeta?.type as DataType,
|
||||
operationType: 'terms',
|
||||
scale: 'ordinal',
|
||||
isBucketed: true,
|
||||
params: {
|
||||
size: 3,
|
||||
orderBy: { type: 'column', columnId: 'y-axis-column' },
|
||||
orderDirection: 'desc',
|
||||
otherBucket: true,
|
||||
missingBucket: false,
|
||||
},
|
||||
};
|
||||
|
||||
this.layers.layer1.columnOrder = ['x-axis-column', 'break-down-column', 'y-axis-column'];
|
||||
|
||||
this.visualization.layers[0].splitAccessor = 'break-down-column';
|
||||
}
|
||||
|
||||
removeBreakdown() {
|
||||
delete this.layers.layer1.columns['break-down-column'];
|
||||
|
||||
this.layers.layer1.columnOrder = ['x-axis-column', 'y-axis-column'];
|
||||
|
||||
this.visualization.layers[0].splitAccessor = undefined;
|
||||
}
|
||||
|
||||
getNumberColumn(sourceField: string): RangeIndexPatternColumn {
|
||||
return {
|
||||
sourceField,
|
||||
label: this.reportViewConfig.labels[sourceField],
|
||||
dataType: 'number',
|
||||
operationType: 'range',
|
||||
isBucketed: true,
|
||||
scale: 'interval',
|
||||
params: {
|
||||
type: 'histogram',
|
||||
ranges: [{ from: 0, to: 1000, label: '' }],
|
||||
maxBars: 'auto',
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
getDateHistogramColumn(sourceField: string): DateHistogramIndexPatternColumn {
|
||||
return {
|
||||
sourceField,
|
||||
dataType: 'date',
|
||||
isBucketed: true,
|
||||
label: '@timestamp',
|
||||
operationType: 'date_histogram',
|
||||
params: { interval: 'auto' },
|
||||
scale: 'interval',
|
||||
};
|
||||
}
|
||||
|
||||
getXAxis():
|
||||
| LastValueIndexPatternColumn
|
||||
| DateHistogramIndexPatternColumn
|
||||
| RangeIndexPatternColumn {
|
||||
const { xAxisColumn } = this.reportViewConfig;
|
||||
|
||||
const { type: fieldType, name: fieldName } = this.getFieldMeta(xAxisColumn.sourceField)!;
|
||||
|
||||
if (fieldType === 'date') {
|
||||
return this.getDateHistogramColumn(fieldName);
|
||||
}
|
||||
if (fieldType === 'number') {
|
||||
return this.getNumberColumn(fieldName);
|
||||
}
|
||||
|
||||
// FIXME review my approach again
|
||||
return this.getDateHistogramColumn(fieldName);
|
||||
}
|
||||
|
||||
getFieldMeta(sourceField?: string) {
|
||||
let xAxisField = sourceField;
|
||||
|
||||
if (xAxisField) {
|
||||
const rdf = this.reportViewConfig.reportDefinitions ?? [];
|
||||
|
||||
const customField = rdf.find(({ field }) => field === xAxisField);
|
||||
|
||||
if (customField) {
|
||||
if (this.reportDefinitions[xAxisField]) {
|
||||
xAxisField = this.reportDefinitions[xAxisField];
|
||||
} else if (customField.defaultValue) {
|
||||
xAxisField = customField.defaultValue;
|
||||
} else if (customField.options?.[0].field) {
|
||||
xAxisField = customField.options?.[0].field;
|
||||
}
|
||||
}
|
||||
|
||||
return this.indexPattern.getFieldByName(xAxisField);
|
||||
}
|
||||
}
|
||||
|
||||
getMainYAxis() {
|
||||
return {
|
||||
dataType: 'number',
|
||||
isBucketed: false,
|
||||
label: 'Count of records',
|
||||
operationType: 'count',
|
||||
scale: 'ratio',
|
||||
sourceField: 'Records',
|
||||
...this.reportViewConfig.yAxisColumn,
|
||||
} as CountIndexPatternColumn;
|
||||
}
|
||||
|
||||
getLayer() {
|
||||
return {
|
||||
columnOrder: ['x-axis-column', 'y-axis-column'],
|
||||
columns: {
|
||||
'x-axis-column': this.getXAxis(),
|
||||
'y-axis-column': this.getMainYAxis(),
|
||||
},
|
||||
incompleteColumns: {},
|
||||
};
|
||||
}
|
||||
|
||||
getXyState(): XYState {
|
||||
return {
|
||||
legend: { isVisible: true, position: 'right' },
|
||||
valueLabels: 'hide',
|
||||
fittingFunction: 'Linear',
|
||||
curveType: 'CURVE_MONOTONE_X' as XYCurveType,
|
||||
axisTitlesVisibilitySettings: { x: true, yLeft: true, yRight: true },
|
||||
tickLabelsVisibilitySettings: { x: true, yLeft: true, yRight: true },
|
||||
gridlinesVisibilitySettings: { x: true, yLeft: true, yRight: true },
|
||||
preferredSeriesType: 'line',
|
||||
layers: [
|
||||
{
|
||||
accessors: ['y-axis-column'],
|
||||
layerId: 'layer1',
|
||||
seriesType: this.seriesType ?? 'line',
|
||||
palette: this.reportViewConfig.palette,
|
||||
yConfig: [{ forAccessor: 'y-axis-column', color: 'green' }],
|
||||
xAccessor: 'x-axis-column',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
parseFilters() {
|
||||
const defaultFilters = this.reportViewConfig.filters ?? [];
|
||||
const parsedFilters = this.reportViewConfig.filters ? [...defaultFilters] : [];
|
||||
|
||||
this.filters.forEach(({ field, values = [], notValues = [] }) => {
|
||||
const fieldMeta = this.indexPattern.fields.find((fieldT) => fieldT.name === field)!;
|
||||
|
||||
if (values?.length > 0) {
|
||||
if (values?.length > 1) {
|
||||
const multiFilter = buildPhrasesFilter(fieldMeta, values, this.indexPattern);
|
||||
parsedFilters.push(multiFilter);
|
||||
} else {
|
||||
const filter = buildPhraseFilter(fieldMeta, values[0], this.indexPattern);
|
||||
parsedFilters.push(filter);
|
||||
}
|
||||
}
|
||||
|
||||
if (notValues?.length > 0) {
|
||||
if (notValues?.length > 1) {
|
||||
const multiFilter = buildPhrasesFilter(fieldMeta, notValues, this.indexPattern);
|
||||
multiFilter.meta.negate = true;
|
||||
parsedFilters.push(multiFilter);
|
||||
} else {
|
||||
const filter = buildPhraseFilter(fieldMeta, notValues[0], this.indexPattern);
|
||||
filter.meta.negate = true;
|
||||
parsedFilters.push(filter);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
return parsedFilters;
|
||||
}
|
||||
|
||||
getJSON(): TypedLensByValueInput['attributes'] {
|
||||
return {
|
||||
title: 'Prefilled from exploratory view app',
|
||||
description: '',
|
||||
visualizationType: 'lnsXY',
|
||||
references: [
|
||||
{
|
||||
id: this.indexPattern.id!,
|
||||
name: 'indexpattern-datasource-current-indexpattern',
|
||||
type: 'index-pattern',
|
||||
},
|
||||
{
|
||||
id: this.indexPattern.id!,
|
||||
name: getLayerReferenceName('layer1'),
|
||||
type: 'index-pattern',
|
||||
},
|
||||
],
|
||||
state: {
|
||||
datasourceStates: {
|
||||
indexpattern: {
|
||||
layers: this.layers,
|
||||
},
|
||||
},
|
||||
visualization: this.visualization,
|
||||
query: { query: '', language: 'kuery' },
|
||||
filters: this.parseFilters(),
|
||||
},
|
||||
};
|
||||
}
|
||||
}
|
|
@ -0,0 +1,39 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DataSeries } from '../types';
|
||||
import { FieldLabels } from './constants';
|
||||
|
||||
interface Props {
|
||||
seriesId: string;
|
||||
}
|
||||
|
||||
export function getLogsFrequencyLensConfig({ seriesId }: Props): DataSeries {
|
||||
return {
|
||||
id: seriesId,
|
||||
reportType: 'logs-frequency',
|
||||
defaultSeriesType: 'line',
|
||||
seriesTypes: ['line', 'bar'],
|
||||
xAxisColumn: {
|
||||
sourceField: '@timestamp',
|
||||
},
|
||||
yAxisColumn: {
|
||||
operationType: 'count',
|
||||
},
|
||||
hasMetricType: false,
|
||||
defaultFilters: [],
|
||||
breakdowns: ['agent.hostname'],
|
||||
filters: [],
|
||||
labels: { ...FieldLabels },
|
||||
reportDefinitions: [
|
||||
{
|
||||
field: 'agent.hostname',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DataSeries } from '../types';
|
||||
import { FieldLabels } from './constants';
|
||||
import { OperationType } from '../../../../../../lens/public';
|
||||
|
||||
interface Props {
|
||||
seriesId: string;
|
||||
}
|
||||
|
||||
export function getMemoryUsageLensConfig({ seriesId }: Props): DataSeries {
|
||||
return {
|
||||
id: seriesId,
|
||||
reportType: 'memory-usage',
|
||||
defaultSeriesType: 'line',
|
||||
seriesTypes: ['line', 'bar'],
|
||||
xAxisColumn: {
|
||||
sourceField: '@timestamp',
|
||||
},
|
||||
yAxisColumn: {
|
||||
operationType: 'avg' as OperationType,
|
||||
sourceField: 'system.memory.used.pct',
|
||||
label: 'Memory Usage %',
|
||||
},
|
||||
hasMetricType: true,
|
||||
defaultFilters: [],
|
||||
breakdowns: ['host.hostname'],
|
||||
filters: [],
|
||||
labels: { ...FieldLabels, 'host.hostname': 'Host name' },
|
||||
reportDefinitions: [
|
||||
{
|
||||
field: 'host.hostname',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
|
@ -0,0 +1,48 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DataSeries } from '../types';
|
||||
import { FieldLabels } from './constants';
|
||||
import { OperationType } from '../../../../../../lens/public';
|
||||
|
||||
interface Props {
|
||||
seriesId: string;
|
||||
}
|
||||
|
||||
export function getMonitorDurationConfig({ seriesId }: Props): DataSeries {
|
||||
return {
|
||||
id: seriesId,
|
||||
reportType: 'uptime-duration',
|
||||
defaultSeriesType: 'line',
|
||||
seriesTypes: ['line', 'bar_stacked'],
|
||||
xAxisColumn: {
|
||||
sourceField: '@timestamp',
|
||||
},
|
||||
yAxisColumn: {
|
||||
operationType: 'avg' as OperationType,
|
||||
sourceField: 'monitor.duration.us',
|
||||
label: 'Monitor duration (ms)',
|
||||
},
|
||||
hasMetricType: true,
|
||||
defaultFilters: ['monitor.type', 'observer.geo.name', 'tags'],
|
||||
breakdowns: [
|
||||
'observer.geo.name',
|
||||
'monitor.name',
|
||||
'monitor.id',
|
||||
'monitor.type',
|
||||
'tags',
|
||||
'url.port',
|
||||
],
|
||||
filters: [],
|
||||
reportDefinitions: [
|
||||
{
|
||||
field: 'monitor.id',
|
||||
},
|
||||
],
|
||||
labels: { ...FieldLabels },
|
||||
};
|
||||
}
|
|
@ -0,0 +1,43 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DataSeries } from '../types';
|
||||
import { FieldLabels } from './constants';
|
||||
|
||||
interface Props {
|
||||
seriesId: string;
|
||||
}
|
||||
|
||||
export function getMonitorPingsConfig({ seriesId }: Props): DataSeries {
|
||||
return {
|
||||
id: seriesId,
|
||||
reportType: 'uptime-pings',
|
||||
defaultSeriesType: 'bar_stacked',
|
||||
seriesTypes: ['bar_stacked', 'bar'],
|
||||
xAxisColumn: {
|
||||
sourceField: '@timestamp',
|
||||
},
|
||||
yAxisColumn: {
|
||||
operationType: 'count',
|
||||
label: 'Monitor pings',
|
||||
},
|
||||
hasMetricType: false,
|
||||
defaultFilters: ['observer.geo.name'],
|
||||
breakdowns: ['monitor.status', 'observer.geo.name', 'monitor.type'],
|
||||
filters: [],
|
||||
palette: { type: 'palette', name: 'status' },
|
||||
reportDefinitions: [
|
||||
{
|
||||
field: 'monitor.id',
|
||||
},
|
||||
{
|
||||
field: 'url.full',
|
||||
},
|
||||
],
|
||||
labels: { ...FieldLabels },
|
||||
};
|
||||
}
|
|
@ -0,0 +1,41 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DataSeries } from '../types';
|
||||
import { FieldLabels } from './constants';
|
||||
import { OperationType } from '../../../../../../lens/public';
|
||||
|
||||
interface Props {
|
||||
seriesId: string;
|
||||
}
|
||||
|
||||
export function getNetworkActivityLensConfig({ seriesId }: Props): DataSeries {
|
||||
return {
|
||||
id: seriesId,
|
||||
reportType: 'network-activity',
|
||||
defaultSeriesType: 'line',
|
||||
seriesTypes: ['line', 'bar'],
|
||||
xAxisColumn: {
|
||||
sourceField: '@timestamp',
|
||||
},
|
||||
yAxisColumn: {
|
||||
operationType: 'avg' as OperationType,
|
||||
sourceField: 'system.memory.used.pct',
|
||||
},
|
||||
hasMetricType: true,
|
||||
defaultFilters: [],
|
||||
breakdowns: ['host.hostname'],
|
||||
filters: [],
|
||||
labels: { ...FieldLabels, 'host.hostname': 'Host name' },
|
||||
reportDefinitions: [
|
||||
{
|
||||
field: 'host.hostname',
|
||||
required: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ConfigProps, DataSeries } from '../types';
|
||||
import { FieldLabels } from './constants';
|
||||
import { buildPhraseFilter } from './utils';
|
||||
import {
|
||||
CLIENT_GEO_COUNTRY_NAME,
|
||||
CLS_FIELD,
|
||||
FCP_FIELD,
|
||||
FID_FIELD,
|
||||
LCP_FIELD,
|
||||
PROCESSOR_EVENT,
|
||||
SERVICE_ENVIRONMENT,
|
||||
SERVICE_NAME,
|
||||
TBT_FIELD,
|
||||
TRANSACTION_DURATION,
|
||||
TRANSACTION_TYPE,
|
||||
USER_AGENT_DEVICE,
|
||||
USER_AGENT_NAME,
|
||||
USER_AGENT_OS,
|
||||
USER_AGENT_VERSION,
|
||||
} from './data/elasticsearch_fieldnames';
|
||||
|
||||
export function getPerformanceDistLensConfig({ seriesId, indexPattern }: ConfigProps): DataSeries {
|
||||
return {
|
||||
id: seriesId ?? 'unique-key',
|
||||
reportType: 'page-load-dist',
|
||||
defaultSeriesType: 'line',
|
||||
seriesTypes: ['line', 'bar'],
|
||||
xAxisColumn: {
|
||||
sourceField: 'performance.metric',
|
||||
},
|
||||
yAxisColumn: {
|
||||
operationType: 'count',
|
||||
label: 'Pages loaded',
|
||||
},
|
||||
hasMetricType: false,
|
||||
defaultFilters: [
|
||||
USER_AGENT_OS,
|
||||
CLIENT_GEO_COUNTRY_NAME,
|
||||
USER_AGENT_DEVICE,
|
||||
{
|
||||
field: USER_AGENT_NAME,
|
||||
nested: USER_AGENT_VERSION,
|
||||
},
|
||||
],
|
||||
breakdowns: [USER_AGENT_NAME, USER_AGENT_OS, CLIENT_GEO_COUNTRY_NAME, USER_AGENT_DEVICE],
|
||||
reportDefinitions: [
|
||||
{
|
||||
field: SERVICE_NAME,
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
field: SERVICE_ENVIRONMENT,
|
||||
},
|
||||
{
|
||||
field: 'performance.metric',
|
||||
custom: true,
|
||||
defaultValue: TRANSACTION_DURATION,
|
||||
options: [
|
||||
{ label: 'Page load time', field: TRANSACTION_DURATION },
|
||||
{ label: 'First contentful paint', field: FCP_FIELD },
|
||||
{ label: 'Total blocking time', field: TBT_FIELD },
|
||||
// FIXME, review if we need these descriptions
|
||||
{ label: 'Largest contentful paint', field: LCP_FIELD, description: 'Core web vital' },
|
||||
{ label: 'First input delay', field: FID_FIELD, description: 'Core web vital' },
|
||||
{ label: 'Cumulative layout shift', field: CLS_FIELD, description: 'Core web vital' },
|
||||
],
|
||||
},
|
||||
],
|
||||
filters: [
|
||||
buildPhraseFilter(TRANSACTION_TYPE, 'page-load', indexPattern),
|
||||
buildPhraseFilter(PROCESSOR_EVENT, 'transaction', indexPattern),
|
||||
],
|
||||
labels: {
|
||||
...FieldLabels,
|
||||
[SERVICE_NAME]: 'Web Application',
|
||||
[TRANSACTION_DURATION]: 'Page load time',
|
||||
},
|
||||
};
|
||||
}
|
|
@ -0,0 +1,52 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ConfigProps, DataSeries } from '../types';
|
||||
import { FieldLabels } from './constants';
|
||||
import { buildPhraseFilter } from './utils';
|
||||
import { OperationType } from '../../../../../../lens/public';
|
||||
|
||||
export function getServiceLatencyLensConfig({ seriesId, indexPattern }: ConfigProps): DataSeries {
|
||||
return {
|
||||
id: seriesId,
|
||||
reportType: 'service-latency',
|
||||
defaultSeriesType: 'line',
|
||||
seriesTypes: ['line', 'bar'],
|
||||
xAxisColumn: {
|
||||
sourceField: '@timestamp',
|
||||
},
|
||||
yAxisColumn: {
|
||||
operationType: 'avg' as OperationType,
|
||||
sourceField: 'transaction.duration.us',
|
||||
label: 'Latency',
|
||||
},
|
||||
hasMetricType: true,
|
||||
defaultFilters: [
|
||||
'user_agent.name',
|
||||
'user_agent.os.name',
|
||||
'client.geo.country_name',
|
||||
'user_agent.device.name',
|
||||
],
|
||||
breakdowns: [
|
||||
'user_agent.name',
|
||||
'user_agent.os.name',
|
||||
'client.geo.country_name',
|
||||
'user_agent.device.name',
|
||||
],
|
||||
filters: [buildPhraseFilter('transaction.type', 'request', indexPattern)],
|
||||
labels: { ...FieldLabels },
|
||||
reportDefinitions: [
|
||||
{
|
||||
field: 'service.name',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
field: 'service.environment',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ConfigProps, DataSeries } from '../types';
|
||||
import { FieldLabels } from './constants';
|
||||
import { buildPhraseFilter } from './utils';
|
||||
import { OperationType } from '../../../../../../lens/public';
|
||||
|
||||
export function getServiceThroughputLensConfig({
|
||||
seriesId,
|
||||
indexPattern,
|
||||
}: ConfigProps): DataSeries {
|
||||
return {
|
||||
id: seriesId,
|
||||
reportType: 'service-latency',
|
||||
defaultSeriesType: 'line',
|
||||
seriesTypes: ['line', 'bar'],
|
||||
xAxisColumn: {
|
||||
sourceField: '@timestamp',
|
||||
},
|
||||
yAxisColumn: {
|
||||
operationType: 'avg' as OperationType,
|
||||
sourceField: 'transaction.duration.us',
|
||||
label: 'Throughput',
|
||||
},
|
||||
hasMetricType: true,
|
||||
defaultFilters: [
|
||||
'user_agent.name',
|
||||
'user_agent.os.name',
|
||||
'client.geo.country_name',
|
||||
'user_agent.device.name',
|
||||
],
|
||||
breakdowns: [
|
||||
'user_agent.name',
|
||||
'user_agent.os.name',
|
||||
'client.geo.country_name',
|
||||
'user_agent.device.name',
|
||||
],
|
||||
filters: [buildPhraseFilter('transaction.type', 'request', indexPattern)],
|
||||
labels: { ...FieldLabels },
|
||||
reportDefinitions: [
|
||||
{
|
||||
field: 'service.name',
|
||||
required: true,
|
||||
},
|
||||
{
|
||||
field: 'service.environment',
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
export enum URL_KEYS {
|
||||
METRIC_TYPE = 'mt',
|
||||
REPORT_TYPE = 'rt',
|
||||
SERIES_TYPE = 'st',
|
||||
BREAK_DOWN = 'bd',
|
||||
FILTERS = 'ft',
|
||||
REPORT_DEFINITIONS = 'rdf',
|
||||
}
|
|
@ -0,0 +1,54 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import rison, { RisonValue } from 'rison-node';
|
||||
import type { AllSeries, AllShortSeries } from '../hooks/use_url_strorage';
|
||||
import type { SeriesUrl } from '../types';
|
||||
import { IIndexPattern } from '../../../../../../../../src/plugins/data/common/index_patterns';
|
||||
import { esFilters } from '../../../../../../../../src/plugins/data/public';
|
||||
import { URL_KEYS } from './url_constants';
|
||||
|
||||
export function convertToShortUrl(series: SeriesUrl) {
|
||||
const {
|
||||
metric,
|
||||
seriesType,
|
||||
reportType,
|
||||
breakdown,
|
||||
filters,
|
||||
reportDefinitions,
|
||||
...restSeries
|
||||
} = series;
|
||||
|
||||
return {
|
||||
[URL_KEYS.METRIC_TYPE]: metric,
|
||||
[URL_KEYS.REPORT_TYPE]: reportType,
|
||||
[URL_KEYS.SERIES_TYPE]: seriesType,
|
||||
[URL_KEYS.BREAK_DOWN]: breakdown,
|
||||
[URL_KEYS.FILTERS]: filters,
|
||||
[URL_KEYS.REPORT_DEFINITIONS]: reportDefinitions,
|
||||
...restSeries,
|
||||
};
|
||||
}
|
||||
|
||||
export function createExploratoryViewUrl(allSeries: AllSeries, baseHref = '') {
|
||||
const allSeriesIds = Object.keys(allSeries);
|
||||
|
||||
const allShortSeries: AllShortSeries = {};
|
||||
|
||||
allSeriesIds.forEach((seriesKey) => {
|
||||
allShortSeries[seriesKey] = convertToShortUrl(allSeries[seriesKey]);
|
||||
});
|
||||
|
||||
return (
|
||||
baseHref +
|
||||
`/app/observability/exploratory-view#?sr=${rison.encode(allShortSeries as RisonValue)}`
|
||||
);
|
||||
}
|
||||
|
||||
export function buildPhraseFilter(field: string, value: any, indexPattern: IIndexPattern) {
|
||||
const fieldMeta = indexPattern.fields.find((fieldT) => fieldT.name === field)!;
|
||||
return esFilters.buildPhraseFilter(fieldMeta, value, indexPattern);
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { within } from '@testing-library/react';
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/dom';
|
||||
import { render, mockUrlStorage, mockCore } from './rtl_helpers';
|
||||
import { ExploratoryView } from './exploratory_view';
|
||||
import { getStubIndexPattern } from '../../../../../../../src/plugins/data/public/test_utils';
|
||||
import * as obsvInd from '../../../utils/observability_index_patterns';
|
||||
|
||||
describe('ExploratoryView', () => {
|
||||
beforeEach(() => {
|
||||
const indexPattern = getStubIndexPattern(
|
||||
'apm-*',
|
||||
() => {},
|
||||
'@timestamp',
|
||||
[
|
||||
{
|
||||
name: '@timestamp',
|
||||
type: 'date',
|
||||
esTypes: ['date'],
|
||||
searchable: true,
|
||||
aggregatable: true,
|
||||
readFromDocValues: true,
|
||||
},
|
||||
],
|
||||
mockCore() as any
|
||||
);
|
||||
|
||||
jest.spyOn(obsvInd, 'ObservabilityIndexPatterns').mockReturnValue({
|
||||
getIndexPattern: jest.fn().mockReturnValue(indexPattern),
|
||||
} as any);
|
||||
});
|
||||
|
||||
it('renders exploratory view', async () => {
|
||||
render(<ExploratoryView />);
|
||||
|
||||
await waitFor(() => {
|
||||
screen.getByText(/open in lens/i);
|
||||
screen.getByRole('heading', { name: /exploratory view/i });
|
||||
screen.getByRole('img', { name: /visulization/i });
|
||||
screen.getByText(/add series/i);
|
||||
screen.getByText(/no series found, please add a series\./i);
|
||||
});
|
||||
});
|
||||
|
||||
it('can add, cancel new series', async () => {
|
||||
render(<ExploratoryView />);
|
||||
|
||||
await fireEvent.click(screen.getByText(/add series/i));
|
||||
|
||||
await waitFor(() => {
|
||||
screen.getByText(/open in lens/i);
|
||||
screen.getByText(/select a data type to start building a series\./i);
|
||||
screen.getByRole('table', { name: /this table contains 1 rows\./i });
|
||||
const button = screen.getByRole('button', { name: /add/i });
|
||||
within(button).getByText(/add/i);
|
||||
});
|
||||
|
||||
await fireEvent.click(screen.getByText(/cancel/i));
|
||||
|
||||
await waitFor(() => {
|
||||
screen.getByText(/add series/i);
|
||||
});
|
||||
});
|
||||
|
||||
it('renders lens component when there is series', async () => {
|
||||
mockUrlStorage({
|
||||
data: {
|
||||
'uptime-pings-histogram': {
|
||||
reportType: 'upp',
|
||||
breakdown: 'monitor.status',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(<ExploratoryView />);
|
||||
|
||||
await waitFor(() => {
|
||||
screen.getByText(/open in lens/i);
|
||||
screen.getByRole('heading', { name: /uptime pings/i });
|
||||
screen.getByText(/uptime-pings-histogram/i);
|
||||
screen.getByText(/Lens Embeddable Component/i);
|
||||
screen.getByRole('table', { name: /this table contains 1 rows\./i });
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,87 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useEffect, useState } from 'react';
|
||||
import styled from 'styled-components';
|
||||
import { EuiLoadingSpinner, EuiPanel, EuiTitle } from '@elastic/eui';
|
||||
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
import { ObservabilityPublicPluginsStart } from '../../../plugin';
|
||||
import { ExploratoryViewHeader } from './header/header';
|
||||
import { SeriesEditor } from './series_editor/series_editor';
|
||||
import { useUrlStorage } from './hooks/use_url_strorage';
|
||||
import { useLensAttributes } from './hooks/use_lens_attributes';
|
||||
import { EmptyView } from './components/empty_view';
|
||||
import { useIndexPatternContext } from './hooks/use_default_index_pattern';
|
||||
import { TypedLensByValueInput } from '../../../../../lens/public';
|
||||
|
||||
export function ExploratoryView() {
|
||||
const {
|
||||
services: { lens },
|
||||
} = useKibana<ObservabilityPublicPluginsStart>();
|
||||
|
||||
const [lensAttributes, setLensAttributes] = useState<TypedLensByValueInput['attributes'] | null>(
|
||||
null
|
||||
);
|
||||
|
||||
const { indexPattern } = useIndexPatternContext();
|
||||
|
||||
const LensComponent = lens?.EmbeddableComponent;
|
||||
|
||||
const { firstSeriesId: seriesId, firstSeries: series } = useUrlStorage();
|
||||
|
||||
const lensAttributesT = useLensAttributes({
|
||||
seriesId,
|
||||
indexPattern,
|
||||
});
|
||||
|
||||
useEffect(() => {
|
||||
setLensAttributes(lensAttributesT);
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
}, [JSON.stringify(lensAttributesT ?? {}), series?.reportType, series?.time?.from]);
|
||||
|
||||
return (
|
||||
<EuiPanel style={{ maxWidth: 1800, minWidth: 1200, margin: '0 auto' }}>
|
||||
{lens ? (
|
||||
<>
|
||||
<ExploratoryViewHeader lensAttributes={lensAttributes} seriesId={seriesId} />
|
||||
{!indexPattern && (
|
||||
<SpinnerWrap>
|
||||
<EuiLoadingSpinner size="xl" />
|
||||
</SpinnerWrap>
|
||||
)}
|
||||
{lensAttributes && seriesId && series?.reportType && series?.time ? (
|
||||
<LensComponent
|
||||
id="exploratoryView"
|
||||
style={{ height: 550 }}
|
||||
timeRange={series?.time}
|
||||
attributes={lensAttributes}
|
||||
/>
|
||||
) : (
|
||||
<EmptyView />
|
||||
)}
|
||||
<SeriesEditor />
|
||||
</>
|
||||
) : (
|
||||
<EuiTitle>
|
||||
<h2>
|
||||
{i18n.translate('xpack.observability.overview.exploratoryView.lensDisabled', {
|
||||
defaultMessage:
|
||||
'Lens app is not available, please enable Lens to use exploratory view.',
|
||||
})}
|
||||
</h2>
|
||||
</EuiTitle>
|
||||
)}
|
||||
</EuiPanel>
|
||||
);
|
||||
}
|
||||
|
||||
const SpinnerWrap = styled.div`
|
||||
height: 100vh;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
`;
|
|
@ -0,0 +1,53 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { mockUrlStorage, render } from '../rtl_helpers';
|
||||
import { ExploratoryViewHeader } from './header';
|
||||
import { fireEvent } from '@testing-library/dom';
|
||||
|
||||
describe('ExploratoryViewHeader', function () {
|
||||
it('should render properly', function () {
|
||||
const { getByText } = render(
|
||||
<ExploratoryViewHeader
|
||||
seriesId={'dummy-series'}
|
||||
lensAttributes={{ title: 'Performance distribution' } as any}
|
||||
/>
|
||||
);
|
||||
getByText('Open in Lens');
|
||||
});
|
||||
|
||||
it('should be able to click open in lens', function () {
|
||||
mockUrlStorage({
|
||||
data: {
|
||||
'uptime-pings-histogram': {
|
||||
reportType: 'upp',
|
||||
breakdown: 'monitor.status',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { getByText, core } = render(
|
||||
<ExploratoryViewHeader
|
||||
seriesId={'dummy-series'}
|
||||
lensAttributes={{ title: 'Performance distribution' } as any}
|
||||
/>
|
||||
);
|
||||
fireEvent.click(getByText('Open in Lens'));
|
||||
|
||||
expect(core?.lens?.navigateToPrefilledEditor).toHaveBeenCalledTimes(1);
|
||||
expect(core?.lens?.navigateToPrefilledEditor).toHaveBeenCalledWith({
|
||||
attributes: { title: 'Performance distribution' },
|
||||
id: '',
|
||||
timeRange: {
|
||||
from: 'now-15m',
|
||||
to: 'now',
|
||||
},
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,63 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
|
||||
import { TypedLensByValueInput } from '../../../../../../lens/public';
|
||||
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
|
||||
import { ObservabilityPublicPluginsStart } from '../../../../plugin';
|
||||
import { DataViewLabels } from '../configurations/constants';
|
||||
import { useUrlStorage } from '../hooks/use_url_strorage';
|
||||
|
||||
interface Props {
|
||||
seriesId: string;
|
||||
lensAttributes: TypedLensByValueInput['attributes'] | null;
|
||||
}
|
||||
|
||||
export function ExploratoryViewHeader({ seriesId, lensAttributes }: Props) {
|
||||
const {
|
||||
services: { lens },
|
||||
} = useKibana<ObservabilityPublicPluginsStart>();
|
||||
|
||||
const { series } = useUrlStorage(seriesId);
|
||||
|
||||
return (
|
||||
<EuiFlexGroup justifyContent="spaceBetween" alignItems="center">
|
||||
<EuiFlexItem>
|
||||
<EuiText>
|
||||
<h2>
|
||||
{DataViewLabels[series.reportType] ??
|
||||
i18n.translate('xpack.observability.expView.heading.label', {
|
||||
defaultMessage: 'Exploratory view',
|
||||
})}
|
||||
</h2>
|
||||
</EuiText>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
iconType="lensApp"
|
||||
fullWidth={false}
|
||||
isDisabled={!lens.canUseEditor() || lensAttributes === null}
|
||||
onClick={() => {
|
||||
if (lensAttributes) {
|
||||
lens.navigateToPrefilledEditor({
|
||||
id: '',
|
||||
timeRange: series.time,
|
||||
attributes: lensAttributes,
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{i18n.translate('xpack.observability.expView.heading.openInLens', {
|
||||
defaultMessage: 'Open in Lens',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { createContext, useContext, Context, useState, useEffect } from 'react';
|
||||
import { IndexPattern } from '../../../../../../../../src/plugins/data/common';
|
||||
import { AppDataType } from '../types';
|
||||
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
|
||||
import { ObservabilityPublicPluginsStart } from '../../../../plugin';
|
||||
import { ObservabilityIndexPatterns } from '../../../../utils/observability_index_patterns';
|
||||
|
||||
export interface IIndexPatternContext {
|
||||
indexPattern: IndexPattern;
|
||||
loadIndexPattern: (dataType: AppDataType) => void;
|
||||
}
|
||||
|
||||
export const IndexPatternContext = createContext<Partial<IIndexPatternContext>>({});
|
||||
|
||||
interface ProviderProps {
|
||||
indexPattern?: IndexPattern;
|
||||
children: JSX.Element;
|
||||
}
|
||||
|
||||
export function IndexPatternContextProvider({
|
||||
children,
|
||||
indexPattern: initialIndexPattern,
|
||||
}: ProviderProps) {
|
||||
const [indexPattern, setIndexPattern] = useState(initialIndexPattern);
|
||||
|
||||
useEffect(() => {
|
||||
setIndexPattern(initialIndexPattern);
|
||||
}, [initialIndexPattern]);
|
||||
|
||||
const {
|
||||
services: { data },
|
||||
} = useKibana<ObservabilityPublicPluginsStart>();
|
||||
|
||||
const loadIndexPattern = async (dataType: AppDataType) => {
|
||||
const obsvIndexP = new ObservabilityIndexPatterns(data);
|
||||
const indPattern = await obsvIndexP.getIndexPattern(dataType);
|
||||
setIndexPattern(indPattern!);
|
||||
};
|
||||
|
||||
return (
|
||||
<IndexPatternContext.Provider
|
||||
value={{
|
||||
indexPattern,
|
||||
loadIndexPattern,
|
||||
}}
|
||||
>
|
||||
{children}
|
||||
</IndexPatternContext.Provider>
|
||||
);
|
||||
}
|
||||
|
||||
export const useIndexPatternContext = () => {
|
||||
return useContext((IndexPatternContext as unknown) as Context<IIndexPatternContext>);
|
||||
};
|
|
@ -0,0 +1,44 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { useFetcher } from '../../../..';
|
||||
import { IKbnUrlStateStorage } from '../../../../../../../../src/plugins/kibana_utils/public';
|
||||
import { useKibana } from '../../../../../../../../src/plugins/kibana_react/public';
|
||||
import { ObservabilityPublicPluginsStart } from '../../../../plugin';
|
||||
import { AllShortSeries } from './use_url_strorage';
|
||||
import { ReportToDataTypeMap } from '../configurations/constants';
|
||||
import {
|
||||
DataType,
|
||||
ObservabilityIndexPatterns,
|
||||
} from '../../../../utils/observability_index_patterns';
|
||||
|
||||
export const useInitExploratoryView = (storage: IKbnUrlStateStorage) => {
|
||||
const {
|
||||
services: { data },
|
||||
} = useKibana<ObservabilityPublicPluginsStart>();
|
||||
|
||||
const allSeriesKey = 'sr';
|
||||
|
||||
const allSeries = storage.get<AllShortSeries>(allSeriesKey) ?? {};
|
||||
|
||||
const allSeriesIds = Object.keys(allSeries);
|
||||
|
||||
const firstSeriesId = allSeriesIds?.[0];
|
||||
|
||||
const firstSeries = allSeries[firstSeriesId];
|
||||
|
||||
const { data: indexPattern } = useFetcher(() => {
|
||||
const obsvIndexP = new ObservabilityIndexPatterns(data);
|
||||
let reportType: DataType = 'apm';
|
||||
if (firstSeries?.rt) {
|
||||
reportType = ReportToDataTypeMap[firstSeries?.rt];
|
||||
}
|
||||
|
||||
return obsvIndexP.getIndexPattern(reportType);
|
||||
}, [firstSeries?.rt, data]);
|
||||
|
||||
return indexPattern;
|
||||
};
|
|
@ -0,0 +1,88 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useMemo } from 'react';
|
||||
import { TypedLensByValueInput } from '../../../../../../lens/public';
|
||||
import { LensAttributes } from '../configurations/lens_attributes';
|
||||
import { useUrlStorage } from './use_url_strorage';
|
||||
import { getDefaultConfigs } from '../configurations/default_configs';
|
||||
|
||||
import { IndexPattern } from '../../../../../../../../src/plugins/data/common';
|
||||
import { DataSeries, SeriesUrl, UrlFilter } from '../types';
|
||||
|
||||
interface Props {
|
||||
seriesId: string;
|
||||
indexPattern?: IndexPattern | null;
|
||||
}
|
||||
|
||||
export const getFiltersFromDefs = (
|
||||
reportDefinitions: SeriesUrl['reportDefinitions'],
|
||||
dataViewConfig: DataSeries
|
||||
) => {
|
||||
const rdfFilters = Object.entries(reportDefinitions ?? {}).map(([field, value]) => {
|
||||
return {
|
||||
field,
|
||||
values: [value],
|
||||
};
|
||||
}) as UrlFilter[];
|
||||
|
||||
// let's filter out custom fields
|
||||
return rdfFilters.filter(({ field }) => {
|
||||
const rdf = dataViewConfig.reportDefinitions.find(({ field: fd }) => field === fd);
|
||||
return !rdf?.custom;
|
||||
});
|
||||
};
|
||||
|
||||
export const useLensAttributes = ({
|
||||
seriesId,
|
||||
indexPattern,
|
||||
}: Props): TypedLensByValueInput['attributes'] | null => {
|
||||
const { series } = useUrlStorage(seriesId);
|
||||
|
||||
const { breakdown, seriesType, metric: metricType, reportType, reportDefinitions = {} } =
|
||||
series ?? {};
|
||||
|
||||
return useMemo(() => {
|
||||
if (!indexPattern || !reportType) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const dataViewConfig = getDefaultConfigs({
|
||||
seriesId,
|
||||
reportType,
|
||||
indexPattern,
|
||||
});
|
||||
|
||||
const filters: UrlFilter[] = (series.filters ?? []).concat(
|
||||
getFiltersFromDefs(reportDefinitions, dataViewConfig)
|
||||
);
|
||||
|
||||
const lensAttributes = new LensAttributes(
|
||||
indexPattern,
|
||||
dataViewConfig,
|
||||
seriesType,
|
||||
filters,
|
||||
metricType,
|
||||
reportDefinitions
|
||||
);
|
||||
|
||||
if (breakdown) {
|
||||
lensAttributes.addBreakdown(breakdown);
|
||||
}
|
||||
|
||||
return lensAttributes.getJSON();
|
||||
}, [
|
||||
indexPattern,
|
||||
breakdown,
|
||||
seriesType,
|
||||
metricType,
|
||||
reportType,
|
||||
reportDefinitions,
|
||||
seriesId,
|
||||
series.filters,
|
||||
]);
|
||||
};
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useUrlStorage } from './use_url_strorage';
|
||||
import { UrlFilter } from '../types';
|
||||
|
||||
export interface UpdateFilter {
|
||||
field: string;
|
||||
value: string;
|
||||
negate?: boolean;
|
||||
}
|
||||
|
||||
export const useSeriesFilters = ({ seriesId }: { seriesId: string }) => {
|
||||
const { series, setSeries } = useUrlStorage(seriesId);
|
||||
|
||||
const filters = series.filters ?? [];
|
||||
|
||||
const removeFilter = ({ field, value, negate }: UpdateFilter) => {
|
||||
const filtersN = filters.map((filter) => {
|
||||
if (filter.field === field) {
|
||||
if (negate) {
|
||||
const notValuesN = filter.notValues?.filter((val) => val !== value);
|
||||
return { ...filter, notValues: notValuesN };
|
||||
} else {
|
||||
const valuesN = filter.values?.filter((val) => val !== value);
|
||||
return { ...filter, values: valuesN };
|
||||
}
|
||||
}
|
||||
|
||||
return filter;
|
||||
});
|
||||
setSeries(seriesId, { ...series, filters: filtersN });
|
||||
};
|
||||
|
||||
const addFilter = ({ field, value, negate }: UpdateFilter) => {
|
||||
const currFilter: UrlFilter = { field };
|
||||
if (negate) {
|
||||
currFilter.notValues = [value];
|
||||
} else {
|
||||
currFilter.values = [value];
|
||||
}
|
||||
if (filters.length === 0) {
|
||||
setSeries(seriesId, { ...series, filters: [currFilter] });
|
||||
} else {
|
||||
setSeries(seriesId, {
|
||||
...series,
|
||||
filters: [currFilter, ...filters.filter((ft) => ft.field !== field)],
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const updateFilter = ({ field, value, negate }: UpdateFilter) => {
|
||||
const currFilter: UrlFilter | undefined = filters.find(({ field: fd }) => field === fd) ?? {
|
||||
field,
|
||||
};
|
||||
|
||||
const currNotValues = currFilter.notValues ?? [];
|
||||
const currValues = currFilter.values ?? [];
|
||||
|
||||
const notValues = currNotValues.filter((val) => val !== value);
|
||||
const values = currValues.filter((val) => val !== value);
|
||||
|
||||
if (negate) {
|
||||
notValues.push(value);
|
||||
} else {
|
||||
values.push(value);
|
||||
}
|
||||
|
||||
currFilter.notValues = notValues.length > 0 ? notValues : undefined;
|
||||
currFilter.values = values.length > 0 ? values : undefined;
|
||||
|
||||
const otherFilters = filters.filter(({ field: fd }) => fd !== field);
|
||||
|
||||
if (notValues.length > 0 || values.length > 0) {
|
||||
setSeries(seriesId, { ...series, filters: [...otherFilters, currFilter] });
|
||||
} else {
|
||||
setSeries(seriesId, { ...series, filters: otherFilters });
|
||||
}
|
||||
};
|
||||
|
||||
const setFilter = ({ field, value, negate }: UpdateFilter) => {
|
||||
const currFilter: UrlFilter | undefined = filters.find(({ field: fd }) => field === fd);
|
||||
|
||||
if (!currFilter) {
|
||||
addFilter({ field, value, negate });
|
||||
} else {
|
||||
updateFilter({ field, value, negate });
|
||||
}
|
||||
};
|
||||
|
||||
const invertFilter = ({ field, value, negate }: UpdateFilter) => {
|
||||
updateFilter({ field, value, negate: !negate });
|
||||
};
|
||||
|
||||
return { invertFilter, setFilter, removeFilter };
|
||||
};
|
|
@ -0,0 +1,103 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { createContext, useContext, Context } from 'react';
|
||||
import { IKbnUrlStateStorage } from '../../../../../../../../src/plugins/kibana_utils/public';
|
||||
import type { AppDataType, ReportViewTypeId, SeriesUrl, UrlFilter } from '../types';
|
||||
import { convertToShortUrl } from '../configurations/utils';
|
||||
import { OperationType, SeriesType } from '../../../../../../lens/public';
|
||||
import { URL_KEYS } from '../configurations/url_constants';
|
||||
|
||||
export const UrlStorageContext = createContext<IKbnUrlStateStorage | null>(null);
|
||||
|
||||
interface ProviderProps {
|
||||
storage: IKbnUrlStateStorage;
|
||||
}
|
||||
|
||||
export function UrlStorageContextProvider({
|
||||
children,
|
||||
storage,
|
||||
}: ProviderProps & { children: JSX.Element }) {
|
||||
return <UrlStorageContext.Provider value={storage}>{children}</UrlStorageContext.Provider>;
|
||||
}
|
||||
|
||||
function convertFromShortUrl(newValue: ShortUrlSeries): SeriesUrl {
|
||||
const { mt, st, rt, bd, ft, time, rdf, ...restSeries } = newValue;
|
||||
return {
|
||||
metric: mt,
|
||||
reportType: rt!,
|
||||
seriesType: st,
|
||||
breakdown: bd,
|
||||
filters: ft!,
|
||||
time: time!,
|
||||
reportDefinitions: rdf,
|
||||
...restSeries,
|
||||
};
|
||||
}
|
||||
|
||||
interface ShortUrlSeries {
|
||||
[URL_KEYS.METRIC_TYPE]?: OperationType;
|
||||
[URL_KEYS.REPORT_TYPE]?: ReportViewTypeId;
|
||||
[URL_KEYS.SERIES_TYPE]?: SeriesType;
|
||||
[URL_KEYS.BREAK_DOWN]?: string;
|
||||
[URL_KEYS.FILTERS]?: UrlFilter[];
|
||||
[URL_KEYS.REPORT_DEFINITIONS]?: Record<string, string>;
|
||||
time?: {
|
||||
to: string;
|
||||
from: string;
|
||||
};
|
||||
dataType?: AppDataType;
|
||||
}
|
||||
|
||||
export type AllShortSeries = Record<string, ShortUrlSeries>;
|
||||
export type AllSeries = Record<string, SeriesUrl>;
|
||||
|
||||
export const NEW_SERIES_KEY = 'newSeriesKey';
|
||||
|
||||
export function useUrlStorage(seriesId?: string) {
|
||||
const allSeriesKey = 'sr';
|
||||
const storage = useContext((UrlStorageContext as unknown) as Context<IKbnUrlStateStorage>);
|
||||
let series: SeriesUrl = {} as SeriesUrl;
|
||||
const allShortSeries = storage.get<AllShortSeries>(allSeriesKey) ?? {};
|
||||
|
||||
const allSeriesIds = Object.keys(allShortSeries);
|
||||
|
||||
const allSeries: AllSeries = {};
|
||||
|
||||
allSeriesIds.forEach((seriesKey) => {
|
||||
allSeries[seriesKey] = convertFromShortUrl(allShortSeries[seriesKey]);
|
||||
});
|
||||
|
||||
if (seriesId) {
|
||||
series = allSeries?.[seriesId] ?? ({} as SeriesUrl);
|
||||
}
|
||||
|
||||
const setSeries = async (seriesIdN: string, newValue: SeriesUrl) => {
|
||||
allShortSeries[seriesIdN] = convertToShortUrl(newValue);
|
||||
allSeries[seriesIdN] = newValue;
|
||||
return storage.set(allSeriesKey, allShortSeries);
|
||||
};
|
||||
|
||||
const removeSeries = (seriesIdN: string) => {
|
||||
delete allShortSeries[seriesIdN];
|
||||
delete allSeries[seriesIdN];
|
||||
storage.set(allSeriesKey, allShortSeries);
|
||||
};
|
||||
|
||||
const firstSeriesId = allSeriesIds?.[0];
|
||||
|
||||
return {
|
||||
storage,
|
||||
setSeries,
|
||||
removeSeries,
|
||||
series,
|
||||
firstSeriesId,
|
||||
allSeries,
|
||||
allSeriesIds,
|
||||
firstSeries: allSeries?.[firstSeriesId],
|
||||
};
|
||||
}
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useContext } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { useHistory } from 'react-router-dom';
|
||||
import { ThemeContext } from 'styled-components';
|
||||
import { ExploratoryView } from './exploratory_view';
|
||||
import { useKibana } from '../../../../../../../src/plugins/kibana_react/public';
|
||||
import { ObservabilityPublicPluginsStart } from '../../../plugin';
|
||||
import { useBreadcrumbs } from '../../../hooks/use_breadcrumbs';
|
||||
import { IndexPatternContextProvider } from './hooks/use_default_index_pattern';
|
||||
import {
|
||||
createKbnUrlStateStorage,
|
||||
withNotifyOnErrors,
|
||||
} from '../../../../../../../src/plugins/kibana_utils/public/';
|
||||
import { UrlStorageContextProvider } from './hooks/use_url_strorage';
|
||||
import { useInitExploratoryView } from './hooks/use_init_exploratory_view';
|
||||
import { WithHeaderLayout } from '../../app/layout/with_header';
|
||||
|
||||
export function ExploratoryViewPage() {
|
||||
useBreadcrumbs([
|
||||
{
|
||||
text: i18n.translate('xpack.observability.overview.exploratoryView', {
|
||||
defaultMessage: 'Exploratory view',
|
||||
}),
|
||||
},
|
||||
]);
|
||||
|
||||
const theme = useContext(ThemeContext);
|
||||
|
||||
const {
|
||||
services: { uiSettings, notifications },
|
||||
} = useKibana<ObservabilityPublicPluginsStart>();
|
||||
|
||||
const history = useHistory();
|
||||
|
||||
const kbnUrlStateStorage = createKbnUrlStateStorage({
|
||||
history,
|
||||
useHash: uiSettings!.get('state:storeInSessionStorage'),
|
||||
...withNotifyOnErrors(notifications!.toasts),
|
||||
});
|
||||
|
||||
const indexPattern = useInitExploratoryView(kbnUrlStateStorage);
|
||||
|
||||
return (
|
||||
<WithHeaderLayout
|
||||
headerColor={theme.eui.euiColorEmptyShade}
|
||||
bodyColor={theme.eui.euiPageBackgroundColor}
|
||||
>
|
||||
{indexPattern ? (
|
||||
<IndexPatternContextProvider indexPattern={indexPattern!}>
|
||||
<UrlStorageContextProvider storage={kbnUrlStateStorage}>
|
||||
<ExploratoryView />
|
||||
</UrlStorageContextProvider>
|
||||
</IndexPatternContextProvider>
|
||||
) : null}
|
||||
</WithHeaderLayout>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,318 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { of } from 'rxjs';
|
||||
import React, { ReactElement } from 'react';
|
||||
import { stringify } from 'query-string';
|
||||
// eslint-disable-next-line import/no-extraneous-dependencies
|
||||
import { render as reactTestLibRender, RenderOptions } from '@testing-library/react';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { createMemoryHistory, History } from 'history';
|
||||
import { CoreStart } from 'kibana/public';
|
||||
import { I18nProvider } from '@kbn/i18n/react';
|
||||
import { coreMock } from 'src/core/public/mocks';
|
||||
import {
|
||||
KibanaServices,
|
||||
KibanaContextProvider,
|
||||
} from '../../../../../../../src/plugins/kibana_react/public';
|
||||
import { ObservabilityPublicPluginsStart } from '../../../plugin';
|
||||
import { EuiThemeProvider } from '../../../../../../../src/plugins/kibana_react/common';
|
||||
import { lensPluginMock } from '../../../../../lens/public/mocks';
|
||||
import { IndexPatternContextProvider } from './hooks/use_default_index_pattern';
|
||||
import { AllSeries, UrlStorageContextProvider } from './hooks/use_url_strorage';
|
||||
import {
|
||||
withNotifyOnErrors,
|
||||
createKbnUrlStateStorage,
|
||||
} from '../../../../../../../src/plugins/kibana_utils/public';
|
||||
import * as fetcherHook from '../../../hooks/use_fetcher';
|
||||
import * as useUrlHook from './hooks/use_url_strorage';
|
||||
import * as useSeriesFilterHook from './hooks/use_series_filters';
|
||||
import * as useHasDataHook from '../../../hooks/use_has_data';
|
||||
import * as useValuesListHook from '../../../hooks/use_values_list';
|
||||
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { getStubIndexPattern } from '../../../../../../../src/plugins/data/public/index_patterns/index_pattern.stub';
|
||||
import indexPatternData from './configurations/data/test_index_pattern.json';
|
||||
|
||||
// eslint-disable-next-line @kbn/eslint/no-restricted-paths
|
||||
import { setIndexPatterns } from '../../../../../../../src/plugins/data/public/services';
|
||||
import { IndexPatternsContract } from '../../../../../../../src/plugins/data/common/index_patterns/index_patterns';
|
||||
import { UrlFilter } from './types';
|
||||
import { dataPluginMock } from '../../../../../../../src/plugins/data/public/mocks';
|
||||
|
||||
interface KibanaProps {
|
||||
services?: KibanaServices;
|
||||
}
|
||||
|
||||
export interface KibanaProviderOptions<ExtraCore> {
|
||||
core?: ExtraCore & Partial<CoreStart>;
|
||||
kibanaProps?: KibanaProps;
|
||||
}
|
||||
|
||||
interface MockKibanaProviderProps<ExtraCore extends Partial<CoreStart>>
|
||||
extends KibanaProviderOptions<ExtraCore> {
|
||||
children: ReactElement;
|
||||
history: History;
|
||||
}
|
||||
|
||||
type MockRouterProps<ExtraCore extends Partial<CoreStart>> = MockKibanaProviderProps<ExtraCore>;
|
||||
|
||||
type Url =
|
||||
| string
|
||||
| {
|
||||
path: string;
|
||||
queryParams: Record<string, string | number>;
|
||||
};
|
||||
|
||||
interface RenderRouterOptions<ExtraCore> extends KibanaProviderOptions<ExtraCore> {
|
||||
history?: History;
|
||||
renderOptions?: Omit<RenderOptions, 'queries'>;
|
||||
url?: Url;
|
||||
}
|
||||
|
||||
function getSetting<T = any>(key: string): T {
|
||||
if (key === 'timepicker:quickRanges') {
|
||||
return ([
|
||||
{
|
||||
display: 'Today',
|
||||
from: 'now/d',
|
||||
to: 'now/d',
|
||||
},
|
||||
] as unknown) as T;
|
||||
}
|
||||
return ('MMM D, YYYY @ HH:mm:ss.SSS' as unknown) as T;
|
||||
}
|
||||
|
||||
function setSetting$<T = any>(key: string): T {
|
||||
return (of('MMM D, YYYY @ HH:mm:ss.SSS') as unknown) as T;
|
||||
}
|
||||
|
||||
/* default mock core */
|
||||
const defaultCore = coreMock.createStart();
|
||||
export const mockCore: () => Partial<CoreStart & ObservabilityPublicPluginsStart> = () => {
|
||||
const core: Partial<CoreStart & ObservabilityPublicPluginsStart> = {
|
||||
...defaultCore,
|
||||
application: {
|
||||
...defaultCore.application,
|
||||
getUrlForApp: () => '/app/observability',
|
||||
navigateToUrl: jest.fn(),
|
||||
capabilities: {
|
||||
...defaultCore.application.capabilities,
|
||||
observability: {
|
||||
'alerting:save': true,
|
||||
configureSettings: true,
|
||||
save: true,
|
||||
show: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
uiSettings: {
|
||||
...defaultCore.uiSettings,
|
||||
get: getSetting,
|
||||
get$: setSetting$,
|
||||
},
|
||||
lens: lensPluginMock.createStartContract(),
|
||||
data: dataPluginMock.createStartContract(),
|
||||
};
|
||||
|
||||
return core;
|
||||
};
|
||||
|
||||
/* Mock Provider Components */
|
||||
export function MockKibanaProvider<ExtraCore extends Partial<CoreStart>>({
|
||||
children,
|
||||
core,
|
||||
history,
|
||||
kibanaProps,
|
||||
}: MockKibanaProviderProps<ExtraCore>) {
|
||||
const { notifications } = core!;
|
||||
|
||||
const kbnUrlStateStorage = createKbnUrlStateStorage({
|
||||
history,
|
||||
useHash: false,
|
||||
...withNotifyOnErrors(notifications!.toasts),
|
||||
});
|
||||
|
||||
const indexPattern = mockIndexPattern;
|
||||
|
||||
setIndexPatterns(({
|
||||
...[indexPattern],
|
||||
get: async () => indexPattern,
|
||||
} as unknown) as IndexPatternsContract);
|
||||
|
||||
return (
|
||||
<KibanaContextProvider services={{ ...core }} {...kibanaProps}>
|
||||
<EuiThemeProvider darkMode={false}>
|
||||
<I18nProvider>
|
||||
<IndexPatternContextProvider indexPattern={indexPattern}>
|
||||
<UrlStorageContextProvider storage={kbnUrlStateStorage}>
|
||||
{children}
|
||||
</UrlStorageContextProvider>
|
||||
</IndexPatternContextProvider>
|
||||
</I18nProvider>
|
||||
</EuiThemeProvider>
|
||||
</KibanaContextProvider>
|
||||
);
|
||||
}
|
||||
|
||||
export function MockRouter<ExtraCore>({
|
||||
children,
|
||||
core,
|
||||
history = createMemoryHistory(),
|
||||
kibanaProps,
|
||||
}: MockRouterProps<ExtraCore>) {
|
||||
return (
|
||||
<Router history={history}>
|
||||
<MockKibanaProvider core={core} kibanaProps={kibanaProps} history={history}>
|
||||
{children}
|
||||
</MockKibanaProvider>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
/* Custom react testing library render */
|
||||
export function render<ExtraCore>(
|
||||
ui: ReactElement,
|
||||
{
|
||||
history = createMemoryHistory(),
|
||||
core: customCore,
|
||||
kibanaProps,
|
||||
renderOptions,
|
||||
url,
|
||||
}: RenderRouterOptions<ExtraCore> = {}
|
||||
) {
|
||||
if (url) {
|
||||
history = getHistoryFromUrl(url);
|
||||
}
|
||||
|
||||
const core = {
|
||||
...mockCore(),
|
||||
...customCore,
|
||||
};
|
||||
|
||||
return {
|
||||
...reactTestLibRender(
|
||||
<MockRouter history={history} kibanaProps={kibanaProps} core={core}>
|
||||
{ui}
|
||||
</MockRouter>,
|
||||
renderOptions
|
||||
),
|
||||
history,
|
||||
core,
|
||||
};
|
||||
}
|
||||
|
||||
const getHistoryFromUrl = (url: Url) => {
|
||||
if (typeof url === 'string') {
|
||||
return createMemoryHistory({
|
||||
initialEntries: [url],
|
||||
});
|
||||
}
|
||||
|
||||
return createMemoryHistory({
|
||||
initialEntries: [url.path + stringify(url.queryParams)],
|
||||
});
|
||||
};
|
||||
|
||||
export const mockFetcher = (data: any) => {
|
||||
return jest.spyOn(fetcherHook, 'useFetcher').mockReturnValue({
|
||||
data,
|
||||
status: fetcherHook.FETCH_STATUS.SUCCESS,
|
||||
refetch: jest.fn(),
|
||||
});
|
||||
};
|
||||
|
||||
export const mockUseHasData = () => {
|
||||
const onRefreshTimeRange = jest.fn();
|
||||
const spy = jest.spyOn(useHasDataHook, 'useHasData').mockReturnValue({
|
||||
onRefreshTimeRange,
|
||||
} as any);
|
||||
return { spy, onRefreshTimeRange };
|
||||
};
|
||||
|
||||
export const mockUseValuesList = (values?: string[]) => {
|
||||
const onRefreshTimeRange = jest.fn();
|
||||
const spy = jest.spyOn(useValuesListHook, 'useValuesList').mockReturnValue({
|
||||
values: values ?? [],
|
||||
} as any);
|
||||
return { spy, onRefreshTimeRange };
|
||||
};
|
||||
|
||||
export const mockUrlStorage = ({
|
||||
data,
|
||||
filters,
|
||||
breakdown,
|
||||
}: {
|
||||
data?: AllSeries;
|
||||
filters?: UrlFilter[];
|
||||
breakdown?: string;
|
||||
}) => {
|
||||
const mockDataSeries = data || {
|
||||
'performance-distribution': {
|
||||
reportType: 'pld',
|
||||
breakdown: breakdown || 'user_agent.name',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
...(filters ? { filters } : {}),
|
||||
},
|
||||
};
|
||||
const allSeriesIds = Object.keys(mockDataSeries);
|
||||
const firstSeriesId = allSeriesIds?.[0];
|
||||
|
||||
const series = mockDataSeries[firstSeriesId];
|
||||
|
||||
const removeSeries = jest.fn();
|
||||
const setSeries = jest.fn();
|
||||
|
||||
const spy = jest.spyOn(useUrlHook, 'useUrlStorage').mockReturnValue({
|
||||
firstSeriesId,
|
||||
allSeriesIds,
|
||||
removeSeries,
|
||||
setSeries,
|
||||
series,
|
||||
firstSeries: mockDataSeries[firstSeriesId],
|
||||
allSeries: mockDataSeries,
|
||||
} as any);
|
||||
|
||||
return { spy, removeSeries, setSeries };
|
||||
};
|
||||
|
||||
export function mockUseSeriesFilter() {
|
||||
const removeFilter = jest.fn();
|
||||
const invertFilter = jest.fn();
|
||||
const setFilter = jest.fn();
|
||||
const spy = jest.spyOn(useSeriesFilterHook, 'useSeriesFilters').mockReturnValue({
|
||||
removeFilter,
|
||||
invertFilter,
|
||||
setFilter,
|
||||
});
|
||||
|
||||
return {
|
||||
spy,
|
||||
removeFilter,
|
||||
invertFilter,
|
||||
setFilter,
|
||||
};
|
||||
}
|
||||
|
||||
const hist = createMemoryHistory();
|
||||
export const mockHistory = {
|
||||
...hist,
|
||||
createHref: jest.fn(({ pathname }) => `/observability${pathname}`),
|
||||
push: jest.fn(),
|
||||
location: {
|
||||
...hist.location,
|
||||
pathname: '/current-path',
|
||||
},
|
||||
};
|
||||
|
||||
export const mockIndexPattern = getStubIndexPattern(
|
||||
'apm-*',
|
||||
() => {},
|
||||
'@timestamp',
|
||||
JSON.parse(indexPatternData.attributes.fields),
|
||||
mockCore() as any
|
||||
);
|
|
@ -0,0 +1,59 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
import { mockUrlStorage, render } from '../../rtl_helpers';
|
||||
import { dataTypes, DataTypesCol } from './data_types_col';
|
||||
import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage';
|
||||
|
||||
describe('DataTypesCol', function () {
|
||||
it('should render properly', function () {
|
||||
const { getByText } = render(<DataTypesCol />);
|
||||
|
||||
dataTypes.forEach(({ label }) => {
|
||||
getByText(label);
|
||||
});
|
||||
});
|
||||
|
||||
it('should set series on change', function () {
|
||||
const { setSeries } = mockUrlStorage({});
|
||||
|
||||
render(<DataTypesCol />);
|
||||
|
||||
fireEvent.click(screen.getByText(/user experience\(rum\)/i));
|
||||
|
||||
expect(setSeries).toHaveBeenCalledTimes(1);
|
||||
expect(setSeries).toHaveBeenCalledWith('newSeriesKey', { dataType: 'rum' });
|
||||
});
|
||||
|
||||
it('should set series on change on already selected', function () {
|
||||
const { setSeries } = mockUrlStorage({
|
||||
data: {
|
||||
[NEW_SERIES_KEY]: {
|
||||
dataType: 'synthetics',
|
||||
reportType: 'upp',
|
||||
breakdown: 'monitor.status',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(<DataTypesCol />);
|
||||
|
||||
const button = screen.getByRole('button', {
|
||||
name: /Synthetic Monitoring/i,
|
||||
});
|
||||
|
||||
expect(button.classList).toContain('euiButton--fill');
|
||||
|
||||
fireEvent.click(button);
|
||||
|
||||
// undefined on click selected
|
||||
expect(setSeries).toHaveBeenCalledWith('newSeriesKey', { dataType: undefined });
|
||||
});
|
||||
});
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiButton, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { AppDataType } from '../../types';
|
||||
import { useIndexPatternContext } from '../../hooks/use_default_index_pattern';
|
||||
import { NEW_SERIES_KEY, useUrlStorage } from '../../hooks/use_url_strorage';
|
||||
|
||||
export const dataTypes: Array<{ id: AppDataType; label: string }> = [
|
||||
{ id: 'synthetics', label: 'Synthetic Monitoring' },
|
||||
{ id: 'rum', label: 'User Experience(RUM)' },
|
||||
{ id: 'logs', label: 'Logs' },
|
||||
{ id: 'metrics', label: 'Metrics' },
|
||||
{ id: 'apm', label: 'APM' },
|
||||
];
|
||||
|
||||
export function DataTypesCol() {
|
||||
const { series, setSeries } = useUrlStorage(NEW_SERIES_KEY);
|
||||
|
||||
const { loadIndexPattern } = useIndexPatternContext();
|
||||
|
||||
const onDataTypeChange = (dataType?: AppDataType) => {
|
||||
if (dataType) {
|
||||
loadIndexPattern(dataType);
|
||||
}
|
||||
setSeries(NEW_SERIES_KEY, { dataType } as any);
|
||||
};
|
||||
|
||||
const selectedDataType = series.dataType;
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="xs">
|
||||
{dataTypes.map(({ id: dataTypeId, label }) => (
|
||||
<EuiFlexItem key={dataTypeId}>
|
||||
<EuiButton
|
||||
size="s"
|
||||
iconSide="right"
|
||||
iconType="arrowRight"
|
||||
color={selectedDataType === dataTypeId ? 'primary' : 'text'}
|
||||
fill={selectedDataType === dataTypeId}
|
||||
onClick={() => {
|
||||
onDataTypeChange(dataTypeId === selectedDataType ? undefined : dataTypeId);
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
import { render } from '../../../../../utils/test_helper';
|
||||
import { getDefaultConfigs } from '../../configurations/default_configs';
|
||||
import { mockIndexPattern, mockUrlStorage } from '../../rtl_helpers';
|
||||
import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage';
|
||||
import { ReportBreakdowns } from './report_breakdowns';
|
||||
import { USER_AGENT_OS } from '../../configurations/data/elasticsearch_fieldnames';
|
||||
|
||||
describe('Series Builder ReportBreakdowns', function () {
|
||||
const dataViewSeries = getDefaultConfigs({
|
||||
reportType: 'pld',
|
||||
indexPattern: mockIndexPattern,
|
||||
seriesId: NEW_SERIES_KEY,
|
||||
});
|
||||
|
||||
it('should render properly', function () {
|
||||
mockUrlStorage({});
|
||||
|
||||
render(<ReportBreakdowns dataViewSeries={dataViewSeries} />);
|
||||
|
||||
screen.getByText('Select an option: , is selected');
|
||||
screen.getAllByText('Browser family');
|
||||
});
|
||||
|
||||
it('should set new series breakdown on change', function () {
|
||||
const { setSeries } = mockUrlStorage({});
|
||||
|
||||
render(<ReportBreakdowns dataViewSeries={dataViewSeries} />);
|
||||
|
||||
const btn = screen.getByRole('button', {
|
||||
name: /select an option: Browser family , is selected/i,
|
||||
hidden: true,
|
||||
});
|
||||
|
||||
fireEvent.click(btn);
|
||||
|
||||
fireEvent.click(screen.getByText(/operating system/i));
|
||||
|
||||
expect(setSeries).toHaveBeenCalledTimes(1);
|
||||
expect(setSeries).toHaveBeenCalledWith('newSeriesKey', {
|
||||
breakdown: USER_AGENT_OS,
|
||||
reportType: 'pld',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
});
|
||||
});
|
||||
it('should set undefined on new series on no select breakdown', function () {
|
||||
const { setSeries } = mockUrlStorage({});
|
||||
|
||||
render(<ReportBreakdowns dataViewSeries={dataViewSeries} />);
|
||||
|
||||
const btn = screen.getByRole('button', {
|
||||
name: /select an option: Browser family , is selected/i,
|
||||
hidden: true,
|
||||
});
|
||||
|
||||
fireEvent.click(btn);
|
||||
|
||||
fireEvent.click(screen.getByText(/no breakdown/i));
|
||||
|
||||
expect(setSeries).toHaveBeenCalledTimes(1);
|
||||
expect(setSeries).toHaveBeenCalledWith('newSeriesKey', {
|
||||
breakdown: undefined,
|
||||
reportType: 'pld',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,15 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { Breakdowns } from '../../series_editor/columns/breakdowns';
|
||||
import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage';
|
||||
import { DataSeries } from '../../types';
|
||||
|
||||
export function ReportBreakdowns({ dataViewSeries }: { dataViewSeries: DataSeries }) {
|
||||
return <Breakdowns breakdowns={dataViewSeries.breakdowns ?? []} seriesId={NEW_SERIES_KEY} />;
|
||||
}
|
|
@ -0,0 +1,75 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
import { getDefaultConfigs } from '../../configurations/default_configs';
|
||||
import { mockIndexPattern, mockUrlStorage, mockUseValuesList, render } from '../../rtl_helpers';
|
||||
import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage';
|
||||
import { ReportDefinitionCol } from './report_definition_col';
|
||||
import { SERVICE_NAME } from '../../configurations/data/elasticsearch_fieldnames';
|
||||
|
||||
describe('Series Builder ReportDefinitionCol', function () {
|
||||
const dataViewSeries = getDefaultConfigs({
|
||||
reportType: 'pld',
|
||||
indexPattern: mockIndexPattern,
|
||||
seriesId: NEW_SERIES_KEY,
|
||||
});
|
||||
|
||||
const { setSeries } = mockUrlStorage({
|
||||
data: {
|
||||
'performance-dist': {
|
||||
dataType: 'rum',
|
||||
reportType: 'pld',
|
||||
time: { from: 'now-30d', to: 'now' },
|
||||
reportDefinitions: { [SERVICE_NAME]: 'elastic-co' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
it('should render properly', async function () {
|
||||
render(<ReportDefinitionCol dataViewSeries={dataViewSeries} />);
|
||||
|
||||
screen.getByText('Web Application');
|
||||
screen.getByText('Environment');
|
||||
screen.getByText('Select an option: Page load time, is selected');
|
||||
screen.getByText('Page load time');
|
||||
});
|
||||
|
||||
it('should render selected report definitions', function () {
|
||||
render(<ReportDefinitionCol dataViewSeries={dataViewSeries} />);
|
||||
|
||||
screen.getByText('elastic-co');
|
||||
});
|
||||
|
||||
it('should be able to remove selected definition', function () {
|
||||
render(<ReportDefinitionCol dataViewSeries={dataViewSeries} />);
|
||||
|
||||
const removeBtn = screen.getByText(/elastic-co/i);
|
||||
|
||||
fireEvent.click(removeBtn);
|
||||
|
||||
expect(setSeries).toHaveBeenCalledTimes(1);
|
||||
expect(setSeries).toHaveBeenCalledWith('newSeriesKey', {
|
||||
dataType: 'rum',
|
||||
reportDefinitions: {},
|
||||
reportType: 'pld',
|
||||
time: { from: 'now-30d', to: 'now' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should be able to unselected selected definition', async function () {
|
||||
mockUseValuesList(['elastic-co']);
|
||||
render(<ReportDefinitionCol dataViewSeries={dataViewSeries} />);
|
||||
|
||||
const definitionBtn = screen.getByText(/web application/i);
|
||||
|
||||
fireEvent.click(definitionBtn);
|
||||
|
||||
screen.getByText('Apply');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,95 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiBadge, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { useIndexPatternContext } from '../../hooks/use_default_index_pattern';
|
||||
import { NEW_SERIES_KEY, useUrlStorage } from '../../hooks/use_url_strorage';
|
||||
import { CustomReportField } from '../custom_report_field';
|
||||
import FieldValueSuggestions from '../../../field_value_suggestions';
|
||||
import { DataSeries } from '../../types';
|
||||
|
||||
export function ReportDefinitionCol({ dataViewSeries }: { dataViewSeries: DataSeries }) {
|
||||
const { indexPattern } = useIndexPatternContext();
|
||||
|
||||
const { series, setSeries } = useUrlStorage(NEW_SERIES_KEY);
|
||||
|
||||
const { reportDefinitions: rtd = {} } = series;
|
||||
|
||||
const { reportDefinitions, labels, filters } = dataViewSeries;
|
||||
|
||||
const onChange = (field: string, value?: string) => {
|
||||
if (!value) {
|
||||
delete rtd[field];
|
||||
setSeries(NEW_SERIES_KEY, {
|
||||
...series,
|
||||
reportDefinitions: { ...rtd },
|
||||
});
|
||||
} else {
|
||||
setSeries(NEW_SERIES_KEY, {
|
||||
...series,
|
||||
reportDefinitions: { ...rtd, [field]: value },
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const onRemove = (field: string) => {
|
||||
delete rtd[field];
|
||||
setSeries(NEW_SERIES_KEY, {
|
||||
...series,
|
||||
reportDefinitions: rtd,
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFlexGroup direction="column" gutterSize="s">
|
||||
{indexPattern &&
|
||||
reportDefinitions.map(({ field, custom, options, defaultValue }) => (
|
||||
<EuiFlexItem key={field}>
|
||||
{!custom ? (
|
||||
<EuiFlexGroup justifyContent="flexStart" gutterSize="s" alignItems="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<FieldValueSuggestions
|
||||
label={labels[field]}
|
||||
sourceField={field}
|
||||
indexPattern={indexPattern}
|
||||
value={rtd?.[field]}
|
||||
onChange={(val?: string) => onChange(field, val)}
|
||||
filters={(filters ?? []).map(({ query }) => query)}
|
||||
time={series.time}
|
||||
width={200}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
{rtd?.[field] && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiBadge
|
||||
iconSide="right"
|
||||
iconType="cross"
|
||||
color="hollow"
|
||||
onClick={() => onRemove(field)}
|
||||
iconOnClick={() => onRemove(field)}
|
||||
iconOnClickAriaLabel={'Click to remove'}
|
||||
onClickAriaLabel={'Click to remove'}
|
||||
>
|
||||
{rtd?.[field]}
|
||||
</EuiBadge>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
) : (
|
||||
<CustomReportField
|
||||
field={field}
|
||||
options={options}
|
||||
defaultValue={defaultValue}
|
||||
seriesId={NEW_SERIES_KEY}
|
||||
/>
|
||||
)}
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,28 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { screen } from '@testing-library/react';
|
||||
import { render } from '../../../../../utils/test_helper';
|
||||
import { ReportFilters } from './report_filters';
|
||||
import { getDefaultConfigs } from '../../configurations/default_configs';
|
||||
import { mockIndexPattern, mockUrlStorage } from '../../rtl_helpers';
|
||||
import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage';
|
||||
|
||||
describe('Series Builder ReportFilters', function () {
|
||||
const dataViewSeries = getDefaultConfigs({
|
||||
reportType: 'pld',
|
||||
indexPattern: mockIndexPattern,
|
||||
seriesId: NEW_SERIES_KEY,
|
||||
});
|
||||
mockUrlStorage({});
|
||||
it('should render properly', function () {
|
||||
render(<ReportFilters dataViewSeries={dataViewSeries} />);
|
||||
|
||||
screen.getByText('Add filter');
|
||||
});
|
||||
});
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { SeriesFilter } from '../../series_editor/columns/series_filter';
|
||||
import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage';
|
||||
import { DataSeries } from '../../types';
|
||||
|
||||
export function ReportFilters({ dataViewSeries }: { dataViewSeries: DataSeries }) {
|
||||
return (
|
||||
<SeriesFilter
|
||||
series={dataViewSeries}
|
||||
defaultFilters={dataViewSeries.defaultFilters}
|
||||
seriesId={NEW_SERIES_KEY}
|
||||
isNew={true}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
import { mockUrlStorage, render } from '../../rtl_helpers';
|
||||
import { ReportTypesCol, SELECTED_DATA_TYPE_FOR_REPORT } from './report_types_col';
|
||||
import { ReportTypes } from '../series_builder';
|
||||
|
||||
describe('ReportTypesCol', function () {
|
||||
it('should render properly', function () {
|
||||
render(<ReportTypesCol reportTypes={ReportTypes.rum} />);
|
||||
screen.getByText('Performance distribution');
|
||||
screen.getByText('KPI over time');
|
||||
});
|
||||
|
||||
it('should display empty message', function () {
|
||||
render(<ReportTypesCol reportTypes={[]} />);
|
||||
screen.getByText(SELECTED_DATA_TYPE_FOR_REPORT);
|
||||
});
|
||||
|
||||
it('should set series on change', function () {
|
||||
const { setSeries } = mockUrlStorage({});
|
||||
render(<ReportTypesCol reportTypes={ReportTypes.synthetics} />);
|
||||
|
||||
fireEvent.click(screen.getByText(/monitor duration/i));
|
||||
|
||||
expect(setSeries).toHaveBeenCalledWith('newSeriesKey', {
|
||||
breakdown: 'user_agent.name',
|
||||
reportDefinitions: {},
|
||||
reportType: 'upd',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
});
|
||||
expect(setSeries).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
it('should set selected as filled', function () {
|
||||
const { setSeries } = mockUrlStorage({
|
||||
data: {
|
||||
newSeriesKey: {
|
||||
dataType: 'synthetics',
|
||||
reportType: 'upp',
|
||||
breakdown: 'monitor.status',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(<ReportTypesCol reportTypes={ReportTypes.synthetics} />);
|
||||
|
||||
const button = screen.getByRole('button', {
|
||||
name: /pings histogram/i,
|
||||
});
|
||||
|
||||
expect(button.classList).toContain('euiButton--fill');
|
||||
fireEvent.click(button);
|
||||
|
||||
// undefined on click selected
|
||||
expect(setSeries).toHaveBeenCalledWith('newSeriesKey', { dataType: 'synthetics' });
|
||||
});
|
||||
});
|
|
@ -0,0 +1,61 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiButton, EuiFlexGroup, EuiFlexItem, EuiText } from '@elastic/eui';
|
||||
import { ReportViewTypeId, SeriesUrl } from '../../types';
|
||||
import { NEW_SERIES_KEY, useUrlStorage } from '../../hooks/use_url_strorage';
|
||||
|
||||
interface Props {
|
||||
reportTypes: Array<{ id: ReportViewTypeId; label: string }>;
|
||||
}
|
||||
|
||||
export function ReportTypesCol({ reportTypes }: Props) {
|
||||
const {
|
||||
series: { reportType: selectedReportType, ...restSeries },
|
||||
setSeries,
|
||||
} = useUrlStorage(NEW_SERIES_KEY);
|
||||
|
||||
return reportTypes?.length > 0 ? (
|
||||
<EuiFlexGroup direction="column" gutterSize="xs">
|
||||
{reportTypes.map(({ id: reportType, label }) => (
|
||||
<EuiFlexItem key={reportType}>
|
||||
<EuiButton
|
||||
size="s"
|
||||
iconSide="right"
|
||||
iconType="arrowRight"
|
||||
color={selectedReportType === reportType ? 'primary' : 'text'}
|
||||
fill={selectedReportType === reportType}
|
||||
onClick={() => {
|
||||
if (reportType === selectedReportType) {
|
||||
setSeries(NEW_SERIES_KEY, {
|
||||
dataType: restSeries.dataType,
|
||||
} as SeriesUrl);
|
||||
} else {
|
||||
setSeries(NEW_SERIES_KEY, {
|
||||
...restSeries,
|
||||
reportType,
|
||||
reportDefinitions: {},
|
||||
});
|
||||
}
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
) : (
|
||||
<EuiText color="subdued">{SELECTED_DATA_TYPE_FOR_REPORT}</EuiText>
|
||||
);
|
||||
}
|
||||
|
||||
export const SELECTED_DATA_TYPE_FOR_REPORT = i18n.translate(
|
||||
'xpack.observability.expView.reportType.noDataType',
|
||||
{ defaultMessage: 'Select a data type to start building a series.' }
|
||||
);
|
|
@ -0,0 +1,47 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiSuperSelect } from '@elastic/eui';
|
||||
import { useUrlStorage } from '../hooks/use_url_strorage';
|
||||
import { ReportDefinition } from '../types';
|
||||
|
||||
interface Props {
|
||||
field: string;
|
||||
seriesId: string;
|
||||
defaultValue?: string;
|
||||
options: ReportDefinition['options'];
|
||||
}
|
||||
|
||||
export function CustomReportField({ field, seriesId, options: opts, defaultValue }: Props) {
|
||||
const { series, setSeries } = useUrlStorage(seriesId);
|
||||
|
||||
const { reportDefinitions: rtd = {} } = series;
|
||||
|
||||
const onChange = (value: string) => {
|
||||
setSeries(seriesId, { ...series, reportDefinitions: { ...rtd, [field]: value } });
|
||||
};
|
||||
|
||||
const { reportDefinitions } = series;
|
||||
|
||||
const NO_SELECT = 'no_select';
|
||||
|
||||
const options = [{ label: 'Select metric', field: NO_SELECT }, ...(opts ?? [])];
|
||||
|
||||
return (
|
||||
<div style={{ maxWidth: 200 }}>
|
||||
<EuiSuperSelect
|
||||
options={options.map(({ label, field: fd, description }) => ({
|
||||
value: fd,
|
||||
inputDisplay: label,
|
||||
}))}
|
||||
valueOfSelected={reportDefinitions?.[field] || defaultValue || NO_SELECT}
|
||||
onChange={(value) => onChange(value)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,201 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiButton, EuiBasicTable, EuiSpacer, EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import styled from 'styled-components';
|
||||
import { AppDataType, ReportViewTypeId, ReportViewTypes, SeriesUrl } from '../types';
|
||||
import { DataTypesCol } from './columns/data_types_col';
|
||||
import { ReportTypesCol } from './columns/report_types_col';
|
||||
import { ReportDefinitionCol } from './columns/report_definition_col';
|
||||
import { ReportFilters } from './columns/report_filters';
|
||||
import { ReportBreakdowns } from './columns/report_breakdowns';
|
||||
import { NEW_SERIES_KEY, useUrlStorage } from '../hooks/use_url_strorage';
|
||||
import { useIndexPatternContext } from '../hooks/use_default_index_pattern';
|
||||
import { getDefaultConfigs } from '../configurations/default_configs';
|
||||
|
||||
export const ReportTypes: Record<AppDataType, Array<{ id: ReportViewTypeId; label: string }>> = {
|
||||
synthetics: [
|
||||
{ id: 'upd', label: 'Monitor duration' },
|
||||
{ id: 'upp', label: 'Pings histogram' },
|
||||
],
|
||||
rum: [
|
||||
{ id: 'pld', label: 'Performance distribution' },
|
||||
{ id: 'kpi', label: 'KPI over time' },
|
||||
],
|
||||
apm: [
|
||||
{ id: 'svl', label: 'Latency' },
|
||||
{ id: 'tpt', label: 'Throughput' },
|
||||
],
|
||||
logs: [
|
||||
{
|
||||
id: 'logs',
|
||||
label: 'Logs Frequency',
|
||||
},
|
||||
],
|
||||
metrics: [
|
||||
{ id: 'cpu', label: 'CPU usage' },
|
||||
{ id: 'mem', label: 'Memory usage' },
|
||||
{ id: 'nwk', label: 'Network activity' },
|
||||
],
|
||||
};
|
||||
|
||||
export function SeriesBuilder() {
|
||||
const { series, setSeries, allSeriesIds, removeSeries } = useUrlStorage(NEW_SERIES_KEY);
|
||||
|
||||
const { dataType, reportType, reportDefinitions = {}, filters = [] } = series;
|
||||
|
||||
const [isFlyoutVisible, setIsFlyoutVisible] = useState(!!series.dataType);
|
||||
|
||||
const { indexPattern } = useIndexPatternContext();
|
||||
|
||||
const getDataViewSeries = () => {
|
||||
return getDefaultConfigs({
|
||||
indexPattern,
|
||||
reportType: reportType!,
|
||||
seriesId: NEW_SERIES_KEY,
|
||||
});
|
||||
};
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: i18n.translate('xpack.observability.expView.seriesBuilder.dataType', {
|
||||
defaultMessage: 'Data Type',
|
||||
}),
|
||||
width: '20%',
|
||||
render: (val: string) => <DataTypesCol />,
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.observability.expView.seriesBuilder.report', {
|
||||
defaultMessage: 'Report',
|
||||
}),
|
||||
width: '20%',
|
||||
render: (val: string) => (
|
||||
<ReportTypesCol reportTypes={dataType ? ReportTypes[dataType] : []} />
|
||||
),
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.observability.expView.seriesBuilder.definition', {
|
||||
defaultMessage: 'Definition',
|
||||
}),
|
||||
width: '30%',
|
||||
render: (val: string) =>
|
||||
reportType && indexPattern ? (
|
||||
<ReportDefinitionCol dataViewSeries={getDataViewSeries()} />
|
||||
) : null,
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.observability.expView.seriesBuilder.filters', {
|
||||
defaultMessage: 'Filters',
|
||||
}),
|
||||
width: '25%',
|
||||
render: (val: string) =>
|
||||
reportType && indexPattern ? <ReportFilters dataViewSeries={getDataViewSeries()} /> : null,
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.observability.expView.seriesBuilder.breakdown', {
|
||||
defaultMessage: 'Breakdowns',
|
||||
}),
|
||||
width: '25%',
|
||||
field: 'id',
|
||||
render: (val: string) =>
|
||||
reportType && indexPattern ? (
|
||||
<ReportBreakdowns dataViewSeries={getDataViewSeries()} />
|
||||
) : null,
|
||||
},
|
||||
];
|
||||
|
||||
const addSeries = () => {
|
||||
if (reportType) {
|
||||
const newSeriesId = `${
|
||||
reportDefinitions?.['service.name'] ||
|
||||
reportDefinitions?.['monitor.id'] ||
|
||||
ReportViewTypes[reportType]
|
||||
}`;
|
||||
|
||||
const newSeriesN = {
|
||||
reportType,
|
||||
time: { from: 'now-30m', to: 'now' },
|
||||
filters,
|
||||
reportDefinitions,
|
||||
} as SeriesUrl;
|
||||
|
||||
setSeries(newSeriesId, newSeriesN).then(() => {
|
||||
removeSeries(NEW_SERIES_KEY);
|
||||
setIsFlyoutVisible(false);
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const items = [{ id: NEW_SERIES_KEY }];
|
||||
|
||||
let flyout;
|
||||
|
||||
if (isFlyoutVisible) {
|
||||
flyout = (
|
||||
<BottomFlyout aria-labelledby="flyoutTitle">
|
||||
<EuiBasicTable
|
||||
items={items as any}
|
||||
columns={columns}
|
||||
cellProps={{ style: { borderRight: '1px solid #d3dae6' } }}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
<EuiFlexGroup justifyContent="flexEnd">
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton fill iconType="plus" color="primary" onClick={addSeries}>
|
||||
{i18n.translate('xpack.observability.expView.seriesBuilder.add', {
|
||||
defaultMessage: 'Add',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButton
|
||||
iconType="cross"
|
||||
color="text"
|
||||
onClick={() => {
|
||||
removeSeries(NEW_SERIES_KEY);
|
||||
setIsFlyoutVisible(false);
|
||||
}}
|
||||
>
|
||||
{i18n.translate('xpack.observability.expView.seriesBuilder.cancel', {
|
||||
defaultMessage: 'Cancel',
|
||||
})}
|
||||
</EuiButton>
|
||||
</EuiFlexItem>
|
||||
</EuiFlexGroup>
|
||||
</BottomFlyout>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<div>
|
||||
{!isFlyoutVisible && (
|
||||
<>
|
||||
<EuiButton
|
||||
iconType={isFlyoutVisible ? 'arrowDown' : 'arrowRight'}
|
||||
color="primary"
|
||||
iconSide="right"
|
||||
onClick={() => setIsFlyoutVisible((prevState) => !prevState)}
|
||||
disabled={allSeriesIds.length > 0}
|
||||
>
|
||||
{i18n.translate('xpack.observability.expView.seriesBuilder.addSeries', {
|
||||
defaultMessage: 'Add series',
|
||||
})}
|
||||
</EuiButton>
|
||||
<EuiSpacer />
|
||||
</>
|
||||
)}
|
||||
{flyout}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
const BottomFlyout = styled.div`
|
||||
height: 300px;
|
||||
`;
|
|
@ -0,0 +1,55 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { EuiSuperDatePicker } from '@elastic/eui';
|
||||
import React, { useEffect } from 'react';
|
||||
import { useHasData } from '../../../../hooks/use_has_data';
|
||||
import { useUrlStorage } from '../hooks/use_url_strorage';
|
||||
import { useQuickTimeRanges } from '../../../../hooks/use_quick_time_ranges';
|
||||
|
||||
export interface TimePickerTime {
|
||||
from: string;
|
||||
to: string;
|
||||
}
|
||||
|
||||
export interface TimePickerQuickRange extends TimePickerTime {
|
||||
display: string;
|
||||
}
|
||||
|
||||
interface Props {
|
||||
seriesId: string;
|
||||
}
|
||||
|
||||
export function SeriesDatePicker({ seriesId }: Props) {
|
||||
const { onRefreshTimeRange } = useHasData();
|
||||
|
||||
const commonlyUsedRanges = useQuickTimeRanges();
|
||||
|
||||
const { series, setSeries } = useUrlStorage(seriesId);
|
||||
|
||||
function onTimeChange({ start, end }: { start: string; end: string }) {
|
||||
onRefreshTimeRange();
|
||||
setSeries(seriesId, { ...series, time: { from: start, to: end } });
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
if (!series || !series.time) {
|
||||
setSeries(seriesId, { ...series, time: { from: 'now-5h', to: 'now' } });
|
||||
}
|
||||
}, [seriesId, series, setSeries]);
|
||||
|
||||
return (
|
||||
<EuiSuperDatePicker
|
||||
start={series?.time?.from}
|
||||
end={series?.time?.to}
|
||||
onTimeChange={onTimeChange}
|
||||
commonlyUsedRanges={commonlyUsedRanges}
|
||||
onRefresh={onTimeChange}
|
||||
showUpdateButton={false}
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,76 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { mockUrlStorage, mockUseHasData, render } from '../rtl_helpers';
|
||||
import { fireEvent, waitFor } from '@testing-library/react';
|
||||
import { SeriesDatePicker } from './index';
|
||||
|
||||
describe('SeriesDatePicker', function () {
|
||||
it('should render properly', function () {
|
||||
mockUrlStorage({
|
||||
data: {
|
||||
'uptime-pings-histogram': {
|
||||
reportType: 'upp',
|
||||
breakdown: 'monitor.status',
|
||||
time: { from: 'now-30m', to: 'now' },
|
||||
},
|
||||
},
|
||||
});
|
||||
const { getByText } = render(<SeriesDatePicker seriesId={'series-id'} />);
|
||||
|
||||
getByText('Last 30 minutes');
|
||||
});
|
||||
|
||||
it('should set defaults', async function () {
|
||||
const { setSeries: setSeries1 } = mockUrlStorage({
|
||||
data: {
|
||||
'uptime-pings-histogram': {
|
||||
reportType: 'upp',
|
||||
breakdown: 'monitor.status',
|
||||
},
|
||||
},
|
||||
} as any);
|
||||
render(<SeriesDatePicker seriesId={'uptime-pings-histogram'} />);
|
||||
expect(setSeries1).toHaveBeenCalledTimes(1);
|
||||
expect(setSeries1).toHaveBeenCalledWith('uptime-pings-histogram', {
|
||||
breakdown: 'monitor.status',
|
||||
reportType: 'upp',
|
||||
time: { from: 'now-5h', to: 'now' },
|
||||
});
|
||||
});
|
||||
|
||||
it('should set series data', async function () {
|
||||
const { setSeries } = mockUrlStorage({
|
||||
data: {
|
||||
'uptime-pings-histogram': {
|
||||
reportType: 'upp',
|
||||
breakdown: 'monitor.status',
|
||||
time: { from: 'now-30m', to: 'now' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const { onRefreshTimeRange } = mockUseHasData();
|
||||
const { getByTestId } = render(<SeriesDatePicker seriesId={'series-id'} />);
|
||||
|
||||
await waitFor(function () {
|
||||
fireEvent.click(getByTestId('superDatePickerToggleQuickMenuButton'));
|
||||
});
|
||||
|
||||
fireEvent.click(getByTestId('superDatePickerCommonlyUsed_Today'));
|
||||
|
||||
expect(onRefreshTimeRange).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(setSeries).toHaveBeenCalledWith('series-id', {
|
||||
breakdown: 'monitor.status',
|
||||
reportType: 'upp',
|
||||
time: { from: 'now/d', to: 'now/d' },
|
||||
});
|
||||
expect(setSeries).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { DataSeries } from '../../types';
|
||||
import { SeriesChartTypes } from './chart_types';
|
||||
import { MetricSelection } from './metric_selection';
|
||||
|
||||
interface Props {
|
||||
series: DataSeries;
|
||||
}
|
||||
|
||||
export function ActionsCol({ series }: Props) {
|
||||
return (
|
||||
<EuiFlexGroup direction="row" gutterSize="s" justifyContent="center">
|
||||
<EuiFlexItem grow={false}>
|
||||
<SeriesChartTypes seriesId={series.id} defaultChartType={series.seriesTypes[0]} />
|
||||
</EuiFlexItem>
|
||||
{series.hasMetricType && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<MetricSelection seriesId={series.id} isDisabled={!series.hasMetricType} />
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,49 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
import { Breakdowns } from './breakdowns';
|
||||
import { mockIndexPattern, mockUrlStorage, render } from '../../rtl_helpers';
|
||||
import { NEW_SERIES_KEY } from '../../hooks/use_url_strorage';
|
||||
import { getDefaultConfigs } from '../../configurations/default_configs';
|
||||
import { USER_AGENT_OS } from '../../configurations/data/elasticsearch_fieldnames';
|
||||
|
||||
describe('Breakdowns', function () {
|
||||
const dataViewSeries = getDefaultConfigs({
|
||||
reportType: 'pld',
|
||||
indexPattern: mockIndexPattern,
|
||||
seriesId: NEW_SERIES_KEY,
|
||||
});
|
||||
|
||||
it('should render properly', async function () {
|
||||
mockUrlStorage({});
|
||||
|
||||
render(<Breakdowns seriesId={'series-id'} breakdowns={dataViewSeries.breakdowns} />);
|
||||
|
||||
screen.getAllByText('Browser family');
|
||||
});
|
||||
|
||||
it('should call set series on change', function () {
|
||||
const { setSeries } = mockUrlStorage({ breakdown: USER_AGENT_OS });
|
||||
|
||||
render(<Breakdowns seriesId={'series-id'} breakdowns={dataViewSeries.breakdowns} />);
|
||||
|
||||
screen.getAllByText('Operating system');
|
||||
|
||||
fireEvent.click(screen.getByTestId('seriesBreakdown'));
|
||||
|
||||
fireEvent.click(screen.getByText('Browser family'));
|
||||
|
||||
expect(setSeries).toHaveBeenCalledWith('series-id', {
|
||||
breakdown: 'user_agent.name',
|
||||
reportType: 'pld',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
});
|
||||
expect(setSeries).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,65 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { EuiSuperSelect } from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { FieldLabels } from '../../configurations/constants';
|
||||
import { useUrlStorage } from '../../hooks/use_url_strorage';
|
||||
|
||||
interface Props {
|
||||
seriesId: string;
|
||||
breakdowns: string[];
|
||||
}
|
||||
|
||||
export function Breakdowns({ seriesId, breakdowns = [] }: Props) {
|
||||
const { setSeries, series } = useUrlStorage(seriesId);
|
||||
|
||||
const selectedBreakdown = series.breakdown;
|
||||
const NO_BREAKDOWN = 'no_breakdown';
|
||||
|
||||
const onOptionChange = (optionId: string) => {
|
||||
if (optionId === NO_BREAKDOWN) {
|
||||
setSeries(seriesId, {
|
||||
...series,
|
||||
breakdown: undefined,
|
||||
});
|
||||
} else {
|
||||
setSeries(seriesId, {
|
||||
...series,
|
||||
breakdown: selectedBreakdown === optionId ? undefined : optionId,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
const items = breakdowns.map((breakdown) => ({ id: breakdown, label: FieldLabels[breakdown] }));
|
||||
items.push({
|
||||
id: NO_BREAKDOWN,
|
||||
label: i18n.translate('xpack.observability.exp.breakDownFilter.noBreakdown', {
|
||||
defaultMessage: 'No breakdown',
|
||||
}),
|
||||
});
|
||||
|
||||
const options = items.map(({ id, label }) => ({
|
||||
inputDisplay: id === NO_BREAKDOWN ? label : <strong>{label}</strong>,
|
||||
value: id,
|
||||
dropdownDisplay: label,
|
||||
}));
|
||||
|
||||
return (
|
||||
<div style={{ width: 200 }}>
|
||||
<EuiSuperSelect
|
||||
fullWidth
|
||||
compressed
|
||||
options={options}
|
||||
valueOfSelected={selectedBreakdown ?? NO_BREAKDOWN}
|
||||
onChange={(value) => onOptionChange(value)}
|
||||
data-test-subj={'seriesBreakdown'}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,56 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { fireEvent, screen, waitFor } from '@testing-library/react';
|
||||
import { SeriesChartTypes, XYChartTypes } from './chart_types';
|
||||
import { mockUrlStorage, render } from '../../rtl_helpers';
|
||||
|
||||
describe.skip('SeriesChartTypes', function () {
|
||||
it('should render properly', async function () {
|
||||
mockUrlStorage({});
|
||||
|
||||
render(<SeriesChartTypes seriesId={'series-id'} defaultChartType={'line'} />);
|
||||
|
||||
await waitFor(() => {
|
||||
screen.getByText(/chart type/i);
|
||||
});
|
||||
});
|
||||
|
||||
it('should call set series on change', async function () {
|
||||
const { setSeries } = mockUrlStorage({});
|
||||
|
||||
render(<SeriesChartTypes seriesId={'series-id'} defaultChartType={'line'} />);
|
||||
|
||||
await waitFor(() => {
|
||||
screen.getByText(/chart type/i);
|
||||
});
|
||||
|
||||
fireEvent.click(screen.getByText(/chart type/i));
|
||||
fireEvent.click(screen.getByTestId('lnsXY_seriesType-bar_stacked'));
|
||||
|
||||
expect(setSeries).toHaveBeenNthCalledWith(1, 'performance-distribution', {
|
||||
breakdown: 'user_agent.name',
|
||||
reportType: 'pld',
|
||||
seriesType: 'bar_stacked',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
});
|
||||
expect(setSeries).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
describe('XYChartTypes', function () {
|
||||
it('should render properly', async function () {
|
||||
mockUrlStorage({});
|
||||
|
||||
render(<XYChartTypes value={'line'} onChange={jest.fn()} label={'Chart type'} />);
|
||||
|
||||
await waitFor(() => {
|
||||
screen.getByText(/chart type/i);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,149 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
|
||||
import {
|
||||
EuiButton,
|
||||
EuiButtonGroup,
|
||||
EuiButtonIcon,
|
||||
EuiLoadingSpinner,
|
||||
EuiPopover,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import styled from 'styled-components';
|
||||
import { useKibana } from '../../../../../../../../../src/plugins/kibana_react/public';
|
||||
import { ObservabilityPublicPluginsStart } from '../../../../../plugin';
|
||||
import { useFetcher } from '../../../../..';
|
||||
import { useUrlStorage } from '../../hooks/use_url_strorage';
|
||||
import { SeriesType } from '../../../../../../../lens/public';
|
||||
|
||||
export function SeriesChartTypes({
|
||||
seriesId,
|
||||
defaultChartType,
|
||||
}: {
|
||||
seriesId: string;
|
||||
defaultChartType: SeriesType;
|
||||
}) {
|
||||
const { series, setSeries, allSeries } = useUrlStorage(seriesId);
|
||||
|
||||
const seriesType = series?.seriesType ?? defaultChartType;
|
||||
|
||||
const onChange = (value: SeriesType) => {
|
||||
Object.keys(allSeries).forEach((seriesKey) => {
|
||||
const seriesN = allSeries[seriesKey];
|
||||
|
||||
setSeries(seriesKey, { ...seriesN, seriesType: value });
|
||||
});
|
||||
};
|
||||
|
||||
return (
|
||||
<XYChartTypes
|
||||
onChange={onChange}
|
||||
value={seriesType}
|
||||
excludeChartTypes={['bar_percentage_stacked']}
|
||||
label={i18n.translate('xpack.observability.expView.chartTypes.label', {
|
||||
defaultMessage: 'Chart type',
|
||||
})}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
export interface XYChartTypesProps {
|
||||
onChange: (value: SeriesType) => void;
|
||||
value: SeriesType;
|
||||
label?: string;
|
||||
includeChartTypes?: string[];
|
||||
excludeChartTypes?: string[];
|
||||
}
|
||||
|
||||
export function XYChartTypes({
|
||||
onChange,
|
||||
value,
|
||||
label,
|
||||
includeChartTypes,
|
||||
excludeChartTypes,
|
||||
}: XYChartTypesProps) {
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const {
|
||||
services: { lens },
|
||||
} = useKibana<ObservabilityPublicPluginsStart>();
|
||||
|
||||
const { data = [], loading } = useFetcher(() => lens.getXyVisTypes(), [lens]);
|
||||
|
||||
let vizTypes = data ?? [];
|
||||
|
||||
if ((excludeChartTypes ?? []).length > 0) {
|
||||
vizTypes = vizTypes.filter(({ id }) => !excludeChartTypes?.includes(id));
|
||||
}
|
||||
|
||||
if ((includeChartTypes ?? []).length > 0) {
|
||||
vizTypes = vizTypes.filter(({ id }) => includeChartTypes?.includes(id));
|
||||
}
|
||||
|
||||
return loading ? (
|
||||
<EuiLoadingSpinner />
|
||||
) : (
|
||||
<EuiPopover
|
||||
isOpen={isOpen}
|
||||
anchorPosition="downCenter"
|
||||
button={
|
||||
label ? (
|
||||
<EuiButton
|
||||
size="s"
|
||||
color="text"
|
||||
iconType={vizTypes.find(({ id }) => id === value)?.icon}
|
||||
onClick={() => {
|
||||
setIsOpen((prevState) => !prevState);
|
||||
}}
|
||||
>
|
||||
{label}
|
||||
</EuiButton>
|
||||
) : (
|
||||
<EuiButtonIcon
|
||||
aria-label={vizTypes.find(({ id }) => id === value)?.label}
|
||||
iconType={vizTypes.find(({ id }) => id === value)?.icon!}
|
||||
onClick={() => {
|
||||
setIsOpen((prevState) => !prevState);
|
||||
}}
|
||||
/>
|
||||
)
|
||||
}
|
||||
closePopover={() => setIsOpen(false)}
|
||||
>
|
||||
<ButtonGroup
|
||||
isIconOnly
|
||||
buttonSize="m"
|
||||
legend={i18n.translate('xpack.observability.xyChart.chartTypeLegend', {
|
||||
defaultMessage: 'Chart type',
|
||||
})}
|
||||
name="chartType"
|
||||
className="eui-displayInlineBlock"
|
||||
options={vizTypes.map((t) => ({
|
||||
id: t.id,
|
||||
label: t.label,
|
||||
title: t.label,
|
||||
iconType: t.icon || 'empty',
|
||||
'data-test-subj': `lnsXY_seriesType-${t.id}`,
|
||||
}))}
|
||||
idSelected={value}
|
||||
onChange={(valueN: string) => {
|
||||
onChange(valueN as SeriesType);
|
||||
}}
|
||||
/>
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
||||
|
||||
const ButtonGroup = styled(EuiButtonGroup)`
|
||||
&&& {
|
||||
.euiButtonGroupButton-isSelected {
|
||||
background-color: #a5a9b1 !important;
|
||||
}
|
||||
}
|
||||
`;
|
|
@ -0,0 +1,20 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { SeriesDatePicker } from '../../series_date_picker';
|
||||
|
||||
interface Props {
|
||||
seriesId: string;
|
||||
}
|
||||
export function DatePickerCol({ seriesId }: Props) {
|
||||
return (
|
||||
<div style={{ maxWidth: 300 }}>
|
||||
<SeriesDatePicker seriesId={seriesId} />
|
||||
</div>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,93 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
import { FilterExpanded } from './filter_expanded';
|
||||
import { mockUrlStorage, mockUseValuesList, render } from '../../rtl_helpers';
|
||||
import { USER_AGENT_NAME } from '../../configurations/data/elasticsearch_fieldnames';
|
||||
|
||||
describe('FilterExpanded', function () {
|
||||
it('should render properly', async function () {
|
||||
mockUrlStorage({ filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] });
|
||||
|
||||
render(
|
||||
<FilterExpanded
|
||||
seriesId={'series-id'}
|
||||
label={'Browser Family'}
|
||||
field={USER_AGENT_NAME}
|
||||
goBack={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
screen.getByText('Browser Family');
|
||||
});
|
||||
it('should call go back on click', async function () {
|
||||
mockUrlStorage({ filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] });
|
||||
const goBack = jest.fn();
|
||||
|
||||
render(
|
||||
<FilterExpanded
|
||||
seriesId={'series-id'}
|
||||
label={'Browser Family'}
|
||||
field={USER_AGENT_NAME}
|
||||
goBack={goBack}
|
||||
/>
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('Browser Family'));
|
||||
|
||||
expect(goBack).toHaveBeenCalledTimes(1);
|
||||
expect(goBack).toHaveBeenCalledWith();
|
||||
});
|
||||
|
||||
it('should call useValuesList on load', async function () {
|
||||
mockUrlStorage({ filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] });
|
||||
|
||||
const { spy } = mockUseValuesList(['Chrome', 'Firefox']);
|
||||
|
||||
const goBack = jest.fn();
|
||||
|
||||
render(
|
||||
<FilterExpanded
|
||||
seriesId={'series-id'}
|
||||
label={'Browser Family'}
|
||||
field={USER_AGENT_NAME}
|
||||
goBack={goBack}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
expect(spy).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
sourceField: USER_AGENT_NAME,
|
||||
})
|
||||
);
|
||||
});
|
||||
it('should filter display values', async function () {
|
||||
mockUrlStorage({ filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] });
|
||||
|
||||
mockUseValuesList(['Chrome', 'Firefox']);
|
||||
|
||||
render(
|
||||
<FilterExpanded
|
||||
seriesId={'series-id'}
|
||||
label={'Browser Family'}
|
||||
field={USER_AGENT_NAME}
|
||||
goBack={jest.fn()}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(screen.queryByText('Firefox')).toBeTruthy();
|
||||
|
||||
fireEvent.input(screen.getByRole('searchbox'), { target: { value: 'ch' } });
|
||||
|
||||
expect(screen.queryByText('Firefox')).toBeFalsy();
|
||||
expect(screen.getByText('Chrome')).toBeTruthy();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,100 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useState, Fragment } from 'react';
|
||||
import {
|
||||
EuiFieldSearch,
|
||||
EuiSpacer,
|
||||
EuiButtonEmpty,
|
||||
EuiLoadingSpinner,
|
||||
EuiFilterGroup,
|
||||
} from '@elastic/eui';
|
||||
import { useIndexPatternContext } from '../../hooks/use_default_index_pattern';
|
||||
import { useUrlStorage } from '../../hooks/use_url_strorage';
|
||||
import { UrlFilter } from '../../types';
|
||||
import { FilterValueButton } from './filter_value_btn';
|
||||
import { useValuesList } from '../../../../../hooks/use_values_list';
|
||||
|
||||
interface Props {
|
||||
seriesId: string;
|
||||
label: string;
|
||||
field: string;
|
||||
goBack: () => void;
|
||||
nestedField?: string;
|
||||
}
|
||||
|
||||
export function FilterExpanded({ seriesId, field, label, goBack, nestedField }: Props) {
|
||||
const { indexPattern } = useIndexPatternContext();
|
||||
|
||||
const [value, setValue] = useState('');
|
||||
|
||||
const [isOpen, setIsOpen] = useState({ value: '', negate: false });
|
||||
|
||||
const { series } = useUrlStorage(seriesId);
|
||||
|
||||
const { values, loading } = useValuesList({
|
||||
sourceField: field,
|
||||
time: series.time,
|
||||
indexPattern,
|
||||
});
|
||||
|
||||
const filters = series?.filters ?? [];
|
||||
|
||||
const currFilter: UrlFilter | undefined = filters.find(({ field: fd }) => field === fd);
|
||||
|
||||
const displayValues = (values || []).filter((opt) =>
|
||||
opt.toLowerCase().includes(value.toLowerCase())
|
||||
);
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiButtonEmpty iconType="arrowLeft" color="text" onClick={() => goBack()}>
|
||||
{label}
|
||||
</EuiButtonEmpty>
|
||||
<EuiFieldSearch
|
||||
fullWidth
|
||||
value={value}
|
||||
onChange={(evt) => {
|
||||
setValue(evt.target.value);
|
||||
}}
|
||||
/>
|
||||
<EuiSpacer size="s" />
|
||||
{loading && (
|
||||
<div style={{ textAlign: 'center' }}>
|
||||
<EuiLoadingSpinner />
|
||||
</div>
|
||||
)}
|
||||
{displayValues.map((opt) => (
|
||||
<Fragment key={opt}>
|
||||
<EuiFilterGroup fullWidth={true} color="primary">
|
||||
<FilterValueButton
|
||||
field={field}
|
||||
value={opt}
|
||||
allSelectedValues={currFilter?.notValues}
|
||||
negate={true}
|
||||
nestedField={nestedField}
|
||||
seriesId={seriesId}
|
||||
isNestedOpen={isOpen}
|
||||
setIsNestedOpen={setIsOpen}
|
||||
/>
|
||||
<FilterValueButton
|
||||
field={field}
|
||||
value={opt}
|
||||
allSelectedValues={currFilter?.values}
|
||||
nestedField={nestedField}
|
||||
seriesId={seriesId}
|
||||
negate={false}
|
||||
isNestedOpen={isOpen}
|
||||
setIsNestedOpen={setIsOpen}
|
||||
/>
|
||||
</EuiFilterGroup>
|
||||
<EuiSpacer size="s" />
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,238 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
import { FilterValueButton } from './filter_value_btn';
|
||||
import { mockUrlStorage, mockUseSeriesFilter, mockUseValuesList, render } from '../../rtl_helpers';
|
||||
import {
|
||||
USER_AGENT_NAME,
|
||||
USER_AGENT_VERSION,
|
||||
} from '../../configurations/data/elasticsearch_fieldnames';
|
||||
|
||||
describe('FilterValueButton', function () {
|
||||
it('should render properly', async function () {
|
||||
render(
|
||||
<FilterValueButton
|
||||
field={USER_AGENT_NAME}
|
||||
seriesId={'series-id'}
|
||||
value={'Chrome'}
|
||||
isNestedOpen={{ value: '', negate: false }}
|
||||
setIsNestedOpen={jest.fn()}
|
||||
negate={false}
|
||||
/>
|
||||
);
|
||||
|
||||
screen.getByText('Chrome');
|
||||
});
|
||||
|
||||
it('should render display negate state', async function () {
|
||||
render(
|
||||
<FilterValueButton
|
||||
field={USER_AGENT_NAME}
|
||||
seriesId={'series-id'}
|
||||
value={'Chrome'}
|
||||
isNestedOpen={{ value: '', negate: false }}
|
||||
setIsNestedOpen={jest.fn()}
|
||||
negate={true}
|
||||
/>
|
||||
);
|
||||
|
||||
screen.getByText('Not Chrome');
|
||||
screen.getByTitle('Not Chrome');
|
||||
const btn = screen.getByRole('button');
|
||||
expect(btn.classList).toContain('euiButtonEmpty--danger');
|
||||
});
|
||||
|
||||
it('should call set filter on click', async function () {
|
||||
const { setFilter, removeFilter } = mockUseSeriesFilter();
|
||||
|
||||
render(
|
||||
<FilterValueButton
|
||||
field={USER_AGENT_NAME}
|
||||
seriesId={'series-id'}
|
||||
value={'Chrome'}
|
||||
isNestedOpen={{ value: '', negate: false }}
|
||||
setIsNestedOpen={jest.fn()}
|
||||
negate={true}
|
||||
allSelectedValues={['Firefox']}
|
||||
/>
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('Not Chrome'));
|
||||
|
||||
expect(removeFilter).toHaveBeenCalledTimes(0);
|
||||
expect(setFilter).toHaveBeenCalledTimes(1);
|
||||
|
||||
expect(setFilter).toHaveBeenCalledWith({
|
||||
field: 'user_agent.name',
|
||||
negate: true,
|
||||
value: 'Chrome',
|
||||
});
|
||||
});
|
||||
it('should remove filter on click if already selected', async function () {
|
||||
mockUrlStorage({});
|
||||
const { removeFilter } = mockUseSeriesFilter();
|
||||
|
||||
render(
|
||||
<FilterValueButton
|
||||
field={USER_AGENT_NAME}
|
||||
seriesId={'series-id'}
|
||||
value={'Chrome'}
|
||||
isNestedOpen={{ value: '', negate: false }}
|
||||
setIsNestedOpen={jest.fn()}
|
||||
negate={false}
|
||||
allSelectedValues={['Chrome', 'Firefox']}
|
||||
/>
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('Chrome'));
|
||||
|
||||
expect(removeFilter).toHaveBeenCalledWith({
|
||||
field: 'user_agent.name',
|
||||
negate: false,
|
||||
value: 'Chrome',
|
||||
});
|
||||
});
|
||||
|
||||
it('should change filter on negated one', async function () {
|
||||
const { removeFilter } = mockUseSeriesFilter();
|
||||
|
||||
render(
|
||||
<FilterValueButton
|
||||
field={USER_AGENT_NAME}
|
||||
seriesId={'series-id'}
|
||||
value={'Chrome'}
|
||||
isNestedOpen={{ value: '', negate: false }}
|
||||
setIsNestedOpen={jest.fn()}
|
||||
negate={true}
|
||||
allSelectedValues={['Chrome', 'Firefox']}
|
||||
/>
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('Not Chrome'));
|
||||
|
||||
expect(removeFilter).toHaveBeenCalledWith({
|
||||
field: 'user_agent.name',
|
||||
negate: true,
|
||||
value: 'Chrome',
|
||||
});
|
||||
});
|
||||
|
||||
it('should force open nested', async function () {
|
||||
mockUseSeriesFilter();
|
||||
const { spy } = mockUseValuesList();
|
||||
|
||||
render(
|
||||
<FilterValueButton
|
||||
field={USER_AGENT_NAME}
|
||||
seriesId={'series-id'}
|
||||
value={'Chrome'}
|
||||
isNestedOpen={{ value: 'Chrome', negate: false }}
|
||||
setIsNestedOpen={jest.fn()}
|
||||
negate={false}
|
||||
allSelectedValues={['Chrome', 'Firefox']}
|
||||
nestedField={USER_AGENT_VERSION}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(1);
|
||||
expect(spy).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
filters: [
|
||||
{
|
||||
term: {
|
||||
[USER_AGENT_NAME]: 'Chrome',
|
||||
},
|
||||
},
|
||||
],
|
||||
sourceField: 'user_agent.version',
|
||||
})
|
||||
);
|
||||
});
|
||||
it('should set isNestedOpen on click', async function () {
|
||||
mockUseSeriesFilter();
|
||||
const { spy } = mockUseValuesList();
|
||||
|
||||
render(
|
||||
<FilterValueButton
|
||||
field={USER_AGENT_NAME}
|
||||
seriesId={'series-id'}
|
||||
value={'Chrome'}
|
||||
isNestedOpen={{ value: 'Chrome', negate: false }}
|
||||
setIsNestedOpen={jest.fn()}
|
||||
negate={false}
|
||||
allSelectedValues={['Chrome', 'Firefox']}
|
||||
nestedField={USER_AGENT_VERSION}
|
||||
/>
|
||||
);
|
||||
|
||||
expect(spy).toHaveBeenCalledTimes(2);
|
||||
expect(spy).toBeCalledWith(
|
||||
expect.objectContaining({
|
||||
filters: [
|
||||
{
|
||||
term: {
|
||||
[USER_AGENT_NAME]: 'Chrome',
|
||||
},
|
||||
},
|
||||
],
|
||||
sourceField: USER_AGENT_VERSION,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should set call setIsNestedOpen on click selected', async function () {
|
||||
mockUseSeriesFilter();
|
||||
mockUseValuesList();
|
||||
|
||||
const setIsNestedOpen = jest.fn();
|
||||
|
||||
render(
|
||||
<FilterValueButton
|
||||
field={USER_AGENT_NAME}
|
||||
seriesId={'series-id'}
|
||||
value={'Chrome'}
|
||||
isNestedOpen={{ value: '', negate: false }}
|
||||
setIsNestedOpen={setIsNestedOpen}
|
||||
negate={false}
|
||||
allSelectedValues={['Chrome', 'Firefox']}
|
||||
nestedField={USER_AGENT_VERSION}
|
||||
/>
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('Chrome'));
|
||||
|
||||
expect(setIsNestedOpen).toHaveBeenCalledTimes(1);
|
||||
expect(setIsNestedOpen).toHaveBeenCalledWith({ negate: false, value: '' });
|
||||
});
|
||||
|
||||
it('should set call setIsNestedOpen on click not selected', async function () {
|
||||
mockUseSeriesFilter();
|
||||
mockUseValuesList();
|
||||
|
||||
const setIsNestedOpen = jest.fn();
|
||||
|
||||
render(
|
||||
<FilterValueButton
|
||||
field={USER_AGENT_NAME}
|
||||
seriesId={'series-id'}
|
||||
value={'Chrome'}
|
||||
isNestedOpen={{ value: '', negate: true }}
|
||||
setIsNestedOpen={setIsNestedOpen}
|
||||
negate={true}
|
||||
allSelectedValues={['Firefox']}
|
||||
nestedField={USER_AGENT_VERSION}
|
||||
/>
|
||||
);
|
||||
|
||||
fireEvent.click(screen.getByText('Not Chrome'));
|
||||
|
||||
expect(setIsNestedOpen).toHaveBeenCalledTimes(1);
|
||||
expect(setIsNestedOpen).toHaveBeenCalledWith({ negate: true, value: 'Chrome' });
|
||||
});
|
||||
});
|
|
@ -0,0 +1,117 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useMemo } from 'react';
|
||||
import { EuiFilterButton, hexToRgb } from '@elastic/eui';
|
||||
import { useIndexPatternContext } from '../../hooks/use_default_index_pattern';
|
||||
import { useUrlStorage } from '../../hooks/use_url_strorage';
|
||||
import { useSeriesFilters } from '../../hooks/use_series_filters';
|
||||
import { euiStyled } from '../../../../../../../../../src/plugins/kibana_react/common';
|
||||
import FieldValueSuggestions from '../../../field_value_suggestions';
|
||||
|
||||
interface Props {
|
||||
value: string;
|
||||
field: string;
|
||||
allSelectedValues?: string[];
|
||||
negate: boolean;
|
||||
nestedField?: string;
|
||||
seriesId: string;
|
||||
isNestedOpen: {
|
||||
value: string;
|
||||
negate: boolean;
|
||||
};
|
||||
setIsNestedOpen: (val: { value: string; negate: boolean }) => void;
|
||||
}
|
||||
|
||||
export function FilterValueButton({
|
||||
isNestedOpen,
|
||||
setIsNestedOpen,
|
||||
value,
|
||||
field,
|
||||
negate,
|
||||
seriesId,
|
||||
nestedField,
|
||||
allSelectedValues,
|
||||
}: Props) {
|
||||
const { series } = useUrlStorage(seriesId);
|
||||
|
||||
const { indexPattern } = useIndexPatternContext();
|
||||
|
||||
const { setFilter, removeFilter } = useSeriesFilters({ seriesId });
|
||||
|
||||
const hasActiveFilters = (allSelectedValues ?? []).includes(value);
|
||||
|
||||
const button = (
|
||||
<FilterButton
|
||||
hasActiveFilters={hasActiveFilters}
|
||||
color={negate ? 'danger' : 'primary'}
|
||||
onClick={() => {
|
||||
if (hasActiveFilters) {
|
||||
removeFilter({ field, value, negate });
|
||||
} else {
|
||||
setFilter({ field, value, negate });
|
||||
}
|
||||
if (!hasActiveFilters) {
|
||||
setIsNestedOpen({ value, negate });
|
||||
} else {
|
||||
setIsNestedOpen({ value: '', negate });
|
||||
}
|
||||
}}
|
||||
>
|
||||
{negate
|
||||
? i18n.translate('xpack.observability.expView.filterValueButton.negate', {
|
||||
defaultMessage: 'Not {value}',
|
||||
values: { value },
|
||||
})
|
||||
: value}
|
||||
</FilterButton>
|
||||
);
|
||||
|
||||
const onNestedChange = (val?: string) => {
|
||||
setFilter({ field: nestedField!, value: val! });
|
||||
setIsNestedOpen({ value: '', negate });
|
||||
};
|
||||
|
||||
const forceOpenNested = isNestedOpen?.value === value && isNestedOpen.negate === negate;
|
||||
|
||||
const filters = useMemo(() => {
|
||||
return [
|
||||
{
|
||||
term: {
|
||||
[field]: value,
|
||||
},
|
||||
},
|
||||
];
|
||||
}, [field, value]);
|
||||
|
||||
return nestedField && forceOpenNested ? (
|
||||
<FieldValueSuggestions
|
||||
button={button}
|
||||
label={'Version'}
|
||||
indexPattern={indexPattern}
|
||||
sourceField={nestedField}
|
||||
onChange={onNestedChange}
|
||||
filters={filters}
|
||||
forceOpen={forceOpenNested}
|
||||
anchorPosition="rightCenter"
|
||||
time={series.time}
|
||||
/>
|
||||
) : (
|
||||
button
|
||||
);
|
||||
}
|
||||
|
||||
const FilterButton = euiStyled(EuiFilterButton)`
|
||||
background-color: rgba(${(props) => {
|
||||
const color = props.hasActiveFilters
|
||||
? props.color === 'danger'
|
||||
? hexToRgb(props.theme.eui.euiColorDanger)
|
||||
: hexToRgb(props.theme.eui.euiColorPrimary)
|
||||
: 'initial';
|
||||
return `${color[0]}, ${color[1]}, ${color[2]}, 0.1`;
|
||||
}});
|
||||
`;
|
|
@ -0,0 +1,112 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { fireEvent, screen } from '@testing-library/react';
|
||||
import { mockUrlStorage, render } from '../../rtl_helpers';
|
||||
import { MetricSelection } from './metric_selection';
|
||||
|
||||
describe('MetricSelection', function () {
|
||||
it('should render properly', function () {
|
||||
render(<MetricSelection seriesId={'series-id'} isDisabled={false} />);
|
||||
|
||||
screen.getByText('Average');
|
||||
});
|
||||
|
||||
it('should display selected value', function () {
|
||||
mockUrlStorage({
|
||||
data: {
|
||||
'performance-distribution': {
|
||||
reportType: 'kpi',
|
||||
metric: 'median',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(<MetricSelection seriesId={'series-id'} isDisabled={false} />);
|
||||
|
||||
screen.getByText('Median');
|
||||
});
|
||||
|
||||
it('should be disabled on disabled state', function () {
|
||||
render(<MetricSelection seriesId={'series-id'} isDisabled={true} />);
|
||||
|
||||
const btn = screen.getByRole('button');
|
||||
|
||||
expect(btn.classList).toContain('euiButton-isDisabled');
|
||||
});
|
||||
|
||||
it('should call set series on change', function () {
|
||||
const { setSeries } = mockUrlStorage({
|
||||
data: {
|
||||
'performance-distribution': {
|
||||
reportType: 'kpi',
|
||||
metric: 'median',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(<MetricSelection seriesId={'series-id'} isDisabled={false} />);
|
||||
|
||||
fireEvent.click(screen.getByText('Median'));
|
||||
|
||||
screen.getByText('Chart metric group');
|
||||
|
||||
fireEvent.click(screen.getByText('95th Percentile'));
|
||||
|
||||
expect(setSeries).toHaveBeenNthCalledWith(1, 'performance-distribution', {
|
||||
metric: '95th',
|
||||
reportType: 'kpi',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
});
|
||||
// FIXME This is a bug in EUI EuiButtonGroup calls on change multiple times
|
||||
// This should be one https://github.com/elastic/eui/issues/4629
|
||||
expect(setSeries).toHaveBeenCalledTimes(3);
|
||||
});
|
||||
|
||||
it('should call set series on change for all series', function () {
|
||||
const { setSeries } = mockUrlStorage({
|
||||
data: {
|
||||
'page-views': {
|
||||
reportType: 'kpi',
|
||||
metric: 'median',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
},
|
||||
'performance-distribution': {
|
||||
reportType: 'kpi',
|
||||
metric: 'median',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
render(<MetricSelection seriesId={'series-id'} isDisabled={false} />);
|
||||
|
||||
fireEvent.click(screen.getByText('Median'));
|
||||
|
||||
screen.getByText('Chart metric group');
|
||||
|
||||
fireEvent.click(screen.getByText('95th Percentile'));
|
||||
|
||||
expect(setSeries).toHaveBeenNthCalledWith(1, 'page-views', {
|
||||
metric: '95th',
|
||||
reportType: 'kpi',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
});
|
||||
|
||||
expect(setSeries).toHaveBeenNthCalledWith(2, 'performance-distribution', {
|
||||
metric: '95th',
|
||||
reportType: 'kpi',
|
||||
time: { from: 'now-15m', to: 'now' },
|
||||
});
|
||||
// FIXME This is a bug in EUI EuiButtonGroup calls on change multiple times
|
||||
// This should be one https://github.com/elastic/eui/issues/4629
|
||||
expect(setSeries).toHaveBeenCalledTimes(6);
|
||||
});
|
||||
});
|
|
@ -0,0 +1,86 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { useState } from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiButton, EuiButtonGroup, EuiPopover } from '@elastic/eui';
|
||||
import { useUrlStorage } from '../../hooks/use_url_strorage';
|
||||
import { OperationType } from '../../../../../../../lens/public';
|
||||
|
||||
const toggleButtons = [
|
||||
{
|
||||
id: `avg`,
|
||||
label: i18n.translate('xpack.observability.expView.metricsSelect.average', {
|
||||
defaultMessage: 'Average',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: `median`,
|
||||
label: i18n.translate('xpack.observability.expView.metricsSelect.median', {
|
||||
defaultMessage: 'Median',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: `95th`,
|
||||
label: i18n.translate('xpack.observability.expView.metricsSelect.9thPercentile', {
|
||||
defaultMessage: '95th Percentile',
|
||||
}),
|
||||
},
|
||||
{
|
||||
id: `99th`,
|
||||
label: i18n.translate('xpack.observability.expView.metricsSelect.99thPercentile', {
|
||||
defaultMessage: '99th Percentile',
|
||||
}),
|
||||
},
|
||||
];
|
||||
|
||||
export function MetricSelection({
|
||||
seriesId,
|
||||
isDisabled,
|
||||
}: {
|
||||
seriesId: string;
|
||||
isDisabled: boolean;
|
||||
}) {
|
||||
const { series, setSeries, allSeries } = useUrlStorage(seriesId);
|
||||
|
||||
const [isOpen, setIsOpen] = useState(false);
|
||||
|
||||
const [toggleIdSelected, setToggleIdSelected] = useState(series?.metric ?? 'avg');
|
||||
|
||||
const onChange = (optionId: OperationType) => {
|
||||
setToggleIdSelected(optionId);
|
||||
|
||||
Object.keys(allSeries).forEach((seriesKey) => {
|
||||
const seriesN = allSeries[seriesKey];
|
||||
|
||||
setSeries(seriesKey, { ...seriesN, metric: optionId });
|
||||
});
|
||||
};
|
||||
const button = (
|
||||
<EuiButton
|
||||
onClick={() => setIsOpen((prevState) => !prevState)}
|
||||
size="s"
|
||||
color="text"
|
||||
isDisabled={isDisabled}
|
||||
>
|
||||
{toggleButtons.find(({ id }) => id === toggleIdSelected)!.label}
|
||||
</EuiButton>
|
||||
);
|
||||
|
||||
return (
|
||||
<EuiPopover button={button} isOpen={isOpen} closePopover={() => setIsOpen(false)}>
|
||||
<EuiButtonGroup
|
||||
buttonSize="m"
|
||||
color="primary"
|
||||
legend="Chart metric group"
|
||||
options={toggleButtons}
|
||||
idSelected={toggleIdSelected}
|
||||
onChange={(id) => onChange(id as OperationType)}
|
||||
/>
|
||||
</EuiPopover>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,35 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React from 'react';
|
||||
import { EuiButtonIcon } from '@elastic/eui';
|
||||
import { DataSeries } from '../../types';
|
||||
import { useUrlStorage } from '../../hooks/use_url_strorage';
|
||||
|
||||
interface Props {
|
||||
series: DataSeries;
|
||||
}
|
||||
|
||||
export function RemoveSeries({ series }: Props) {
|
||||
const { removeSeries } = useUrlStorage();
|
||||
|
||||
const onClick = () => {
|
||||
removeSeries(series.id);
|
||||
};
|
||||
return (
|
||||
<EuiButtonIcon
|
||||
aria-label={i18n.translate('xpack.observability.expView.seriesEditor.removeSeries', {
|
||||
defaultMessage: 'Click to remove series',
|
||||
})}
|
||||
iconType="cross"
|
||||
color="primary"
|
||||
onClick={onClick}
|
||||
size="m"
|
||||
/>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import React, { useState, Fragment } from 'react';
|
||||
import {
|
||||
EuiButton,
|
||||
EuiPopover,
|
||||
EuiSpacer,
|
||||
EuiButtonEmpty,
|
||||
EuiFlexItem,
|
||||
EuiFlexGroup,
|
||||
} from '@elastic/eui';
|
||||
import { FilterExpanded } from './filter_expanded';
|
||||
import { DataSeries } from '../../types';
|
||||
import { FieldLabels } from '../../configurations/constants';
|
||||
import { SelectedFilters } from '../selected_filters';
|
||||
import { NEW_SERIES_KEY, useUrlStorage } from '../../hooks/use_url_strorage';
|
||||
|
||||
interface Props {
|
||||
seriesId: string;
|
||||
defaultFilters: DataSeries['defaultFilters'];
|
||||
series: DataSeries;
|
||||
isNew?: boolean;
|
||||
}
|
||||
|
||||
export interface Field {
|
||||
label: string;
|
||||
field: string;
|
||||
nested?: string;
|
||||
}
|
||||
|
||||
export function SeriesFilter({ series, isNew, seriesId, defaultFilters = [] }: Props) {
|
||||
const [isPopoverVisible, setIsPopoverVisible] = useState(false);
|
||||
|
||||
const [selectedField, setSelectedField] = useState<Field | undefined>();
|
||||
|
||||
const options = defaultFilters.map((field) => {
|
||||
if (typeof field === 'string') {
|
||||
return { label: FieldLabels[field], field };
|
||||
}
|
||||
return { label: FieldLabels[field.field], field: field.field, nested: field.nested };
|
||||
});
|
||||
const disabled = seriesId === NEW_SERIES_KEY && !isNew;
|
||||
|
||||
const { setSeries, series: urlSeries } = useUrlStorage(seriesId);
|
||||
|
||||
const button = (
|
||||
<EuiButtonEmpty
|
||||
flush="left"
|
||||
iconType="plus"
|
||||
onClick={() => {
|
||||
setIsPopoverVisible(true);
|
||||
}}
|
||||
isDisabled={disabled}
|
||||
size="s"
|
||||
>
|
||||
{i18n.translate('xpack.observability.expView.seriesEditor.addFilter', {
|
||||
defaultMessage: 'Add filter',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
);
|
||||
|
||||
const mainPanel = (
|
||||
<>
|
||||
<EuiSpacer size="s" />
|
||||
{options.map((opt) => (
|
||||
<Fragment key={opt.label}>
|
||||
<EuiButton
|
||||
fullWidth={true}
|
||||
iconType="arrowRight"
|
||||
iconSide="right"
|
||||
onClick={() => {
|
||||
setSelectedField(opt);
|
||||
}}
|
||||
>
|
||||
{opt.label}
|
||||
</EuiButton>
|
||||
<EuiSpacer size="s" />
|
||||
</Fragment>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
|
||||
const childPanel = selectedField ? (
|
||||
<FilterExpanded
|
||||
seriesId={seriesId}
|
||||
field={selectedField.field}
|
||||
label={selectedField.label}
|
||||
nestedField={selectedField.nested}
|
||||
goBack={() => {
|
||||
setSelectedField(undefined);
|
||||
}}
|
||||
/>
|
||||
) : null;
|
||||
|
||||
const closePopover = () => {
|
||||
setIsPopoverVisible(false);
|
||||
setSelectedField(undefined);
|
||||
};
|
||||
|
||||
return (
|
||||
<EuiFlexGroup wrap direction="column" gutterSize="xs" alignItems="flexStart">
|
||||
{!disabled && <SelectedFilters seriesId={seriesId} series={series} isNew={isNew} />}
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiPopover
|
||||
button={button}
|
||||
isOpen={isPopoverVisible}
|
||||
closePopover={closePopover}
|
||||
anchorPosition="leftCenter"
|
||||
>
|
||||
{!selectedField ? mainPanel : childPanel}
|
||||
</EuiPopover>
|
||||
</EuiFlexItem>
|
||||
{(urlSeries.filters ?? []).length > 0 && (
|
||||
<EuiFlexItem grow={false}>
|
||||
<EuiButtonEmpty
|
||||
flush="left"
|
||||
color="text"
|
||||
iconType="cross"
|
||||
onClick={() => {
|
||||
setSeries(seriesId, { ...urlSeries, filters: undefined });
|
||||
}}
|
||||
isDisabled={disabled}
|
||||
size="s"
|
||||
>
|
||||
{i18n.translate('xpack.observability.expView.seriesEditor.clearFilter', {
|
||||
defaultMessage: 'Clear filters',
|
||||
})}
|
||||
</EuiButtonEmpty>
|
||||
</EuiFlexItem>
|
||||
)}
|
||||
</EuiFlexGroup>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { screen, waitFor } from '@testing-library/react';
|
||||
import { mockIndexPattern, mockUrlStorage, render } from '../rtl_helpers';
|
||||
import { SelectedFilters } from './selected_filters';
|
||||
import { getDefaultConfigs } from '../configurations/default_configs';
|
||||
import { NEW_SERIES_KEY } from '../hooks/use_url_strorage';
|
||||
import { USER_AGENT_NAME } from '../configurations/data/elasticsearch_fieldnames';
|
||||
|
||||
describe('SelectedFilters', function () {
|
||||
const dataViewSeries = getDefaultConfigs({
|
||||
reportType: 'pld',
|
||||
indexPattern: mockIndexPattern,
|
||||
seriesId: NEW_SERIES_KEY,
|
||||
});
|
||||
|
||||
it('should render properly', async function () {
|
||||
mockUrlStorage({ filters: [{ field: USER_AGENT_NAME, values: ['Chrome'] }] });
|
||||
|
||||
render(<SelectedFilters seriesId={'series-id'} series={dataViewSeries} />);
|
||||
|
||||
await waitFor(() => {
|
||||
screen.getByText('Chrome');
|
||||
screen.getByTitle('Filter: Browser family: Chrome. Select for more filter actions.');
|
||||
});
|
||||
});
|
||||
});
|
|
@ -0,0 +1,96 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React, { Fragment } from 'react';
|
||||
import { EuiFlexGroup, EuiFlexItem } from '@elastic/eui';
|
||||
import { NEW_SERIES_KEY, useUrlStorage } from '../hooks/use_url_strorage';
|
||||
import { FilterLabel } from '../components/filter_label';
|
||||
import { DataSeries, UrlFilter } from '../types';
|
||||
import { useIndexPatternContext } from '../hooks/use_default_index_pattern';
|
||||
import { useSeriesFilters } from '../hooks/use_series_filters';
|
||||
import { getFiltersFromDefs } from '../hooks/use_lens_attributes';
|
||||
|
||||
interface Props {
|
||||
seriesId: string;
|
||||
series: DataSeries;
|
||||
isNew?: boolean;
|
||||
}
|
||||
export function SelectedFilters({ seriesId, isNew, series: dataSeries }: Props) {
|
||||
const { series } = useUrlStorage(seriesId);
|
||||
|
||||
const { reportDefinitions = {} } = series;
|
||||
|
||||
const { labels } = dataSeries;
|
||||
|
||||
const filters: UrlFilter[] = series.filters ?? [];
|
||||
|
||||
let definitionFilters: UrlFilter[] = getFiltersFromDefs(reportDefinitions, dataSeries);
|
||||
|
||||
// we don't want to display report definition filters in new series view
|
||||
if (seriesId === NEW_SERIES_KEY && isNew) {
|
||||
definitionFilters = [];
|
||||
}
|
||||
|
||||
const { removeFilter } = useSeriesFilters({ seriesId });
|
||||
|
||||
const { indexPattern } = useIndexPatternContext();
|
||||
|
||||
return (filters.length > 0 || definitionFilters.length > 0) && indexPattern ? (
|
||||
<EuiFlexItem>
|
||||
<EuiFlexGroup wrap gutterSize="xs">
|
||||
{filters.map(({ field, values, notValues }) => (
|
||||
<Fragment key={field}>
|
||||
{(values ?? []).map((val) => (
|
||||
<EuiFlexItem key={field + val} grow={false}>
|
||||
<FilterLabel
|
||||
seriesId={seriesId}
|
||||
field={field}
|
||||
label={labels[field]}
|
||||
value={val}
|
||||
removeFilter={() => removeFilter({ field, value: val, negate: false })}
|
||||
negate={false}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
{(notValues ?? []).map((val) => (
|
||||
<EuiFlexItem key={field + val} grow={false}>
|
||||
<FilterLabel
|
||||
seriesId={seriesId}
|
||||
field={field}
|
||||
label={labels[field]}
|
||||
value={val}
|
||||
negate={true}
|
||||
removeFilter={() => removeFilter({ field, value: val, negate: true })}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</Fragment>
|
||||
))}
|
||||
|
||||
{definitionFilters.map(({ field, values }) => (
|
||||
<Fragment key={field}>
|
||||
{(values ?? []).map((val) => (
|
||||
<EuiFlexItem key={field + val} grow={false}>
|
||||
<FilterLabel
|
||||
seriesId={seriesId}
|
||||
field={field}
|
||||
label={labels[field]}
|
||||
value={val}
|
||||
removeFilter={() => {
|
||||
// FIXME handle this use case
|
||||
}}
|
||||
negate={false}
|
||||
definitionFilter={true}
|
||||
/>
|
||||
</EuiFlexItem>
|
||||
))}
|
||||
</Fragment>
|
||||
))}
|
||||
</EuiFlexGroup>
|
||||
</EuiFlexItem>
|
||||
) : null;
|
||||
}
|
|
@ -0,0 +1,139 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import React from 'react';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { EuiBasicTable, EuiIcon, EuiSpacer, EuiText } from '@elastic/eui';
|
||||
import { SeriesFilter } from './columns/series_filter';
|
||||
import { ActionsCol } from './columns/actions_col';
|
||||
import { Breakdowns } from './columns/breakdowns';
|
||||
import { DataSeries } from '../types';
|
||||
import { SeriesBuilder } from '../series_builder/series_builder';
|
||||
import { NEW_SERIES_KEY, useUrlStorage } from '../hooks/use_url_strorage';
|
||||
import { getDefaultConfigs } from '../configurations/default_configs';
|
||||
import { DatePickerCol } from './columns/date_picker_col';
|
||||
import { RemoveSeries } from './columns/remove_series';
|
||||
import { useIndexPatternContext } from '../hooks/use_default_index_pattern';
|
||||
|
||||
export function SeriesEditor() {
|
||||
const { allSeries, firstSeriesId } = useUrlStorage();
|
||||
|
||||
const columns = [
|
||||
{
|
||||
name: i18n.translate('xpack.observability.expView.seriesEditor.name', {
|
||||
defaultMessage: 'Name',
|
||||
}),
|
||||
field: 'id',
|
||||
width: '15%',
|
||||
render: (val: string) => (
|
||||
<EuiText>
|
||||
<EuiIcon type="dot" color="green" size="l" />{' '}
|
||||
{val === NEW_SERIES_KEY ? 'new-series-preview' : val}
|
||||
</EuiText>
|
||||
),
|
||||
},
|
||||
...(firstSeriesId !== NEW_SERIES_KEY
|
||||
? [
|
||||
{
|
||||
name: i18n.translate('xpack.observability.expView.seriesEditor.filters', {
|
||||
defaultMessage: 'Filters',
|
||||
}),
|
||||
field: 'defaultFilters',
|
||||
width: '25%',
|
||||
render: (defaultFilters: string[], series: DataSeries) => (
|
||||
<SeriesFilter defaultFilters={defaultFilters} seriesId={series.id} series={series} />
|
||||
),
|
||||
},
|
||||
{
|
||||
name: i18n.translate('xpack.observability.expView.seriesEditor.breakdowns', {
|
||||
defaultMessage: 'Breakdowns',
|
||||
}),
|
||||
field: 'breakdowns',
|
||||
width: '15%',
|
||||
render: (val: string[], item: DataSeries) => (
|
||||
<Breakdowns seriesId={item.id} breakdowns={val} />
|
||||
),
|
||||
},
|
||||
{
|
||||
name: '',
|
||||
align: 'center' as const,
|
||||
width: '15%',
|
||||
field: 'id',
|
||||
render: (val: string, item: DataSeries) => <ActionsCol series={item} />,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
{
|
||||
name: (
|
||||
<div>
|
||||
{i18n.translate('xpack.observability.expView.seriesEditor.time', {
|
||||
defaultMessage: 'Time',
|
||||
})}
|
||||
</div>
|
||||
),
|
||||
width: '20%',
|
||||
field: 'id',
|
||||
align: 'right' as const,
|
||||
render: (val: string, item: DataSeries) => <DatePickerCol seriesId={item.id} />,
|
||||
},
|
||||
|
||||
...(firstSeriesId !== NEW_SERIES_KEY
|
||||
? [
|
||||
{
|
||||
name: i18n.translate('xpack.observability.expView.seriesEditor.actions', {
|
||||
defaultMessage: 'Actions',
|
||||
}),
|
||||
align: 'center' as const,
|
||||
width: '5%',
|
||||
field: 'id',
|
||||
render: (val: string, item: DataSeries) => <RemoveSeries series={item} />,
|
||||
},
|
||||
]
|
||||
: []),
|
||||
];
|
||||
|
||||
const allSeriesKeys = Object.keys(allSeries);
|
||||
|
||||
const items: DataSeries[] = [];
|
||||
|
||||
const { indexPattern } = useIndexPatternContext();
|
||||
|
||||
allSeriesKeys.forEach((seriesKey) => {
|
||||
const series = allSeries[seriesKey];
|
||||
if (series.reportType && indexPattern) {
|
||||
items.push(
|
||||
getDefaultConfigs({
|
||||
indexPattern,
|
||||
reportType: series.reportType,
|
||||
seriesId: seriesKey,
|
||||
})
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<EuiSpacer />
|
||||
<EuiBasicTable
|
||||
items={items}
|
||||
rowHeader="firstName"
|
||||
columns={columns}
|
||||
rowProps={() => (firstSeriesId === NEW_SERIES_KEY ? {} : { height: 100 })}
|
||||
noItemsMessage={i18n.translate('xpack.observability.expView.seriesEditor.notFound', {
|
||||
defaultMessage: 'No series found, please add a series.',
|
||||
})}
|
||||
cellProps={{
|
||||
style: {
|
||||
verticalAlign: 'top',
|
||||
},
|
||||
}}
|
||||
/>
|
||||
<EuiSpacer />
|
||||
<SeriesBuilder />
|
||||
</>
|
||||
);
|
||||
}
|
|
@ -0,0 +1,89 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { PaletteOutput } from 'src/plugins/charts/public';
|
||||
import {
|
||||
LastValueIndexPatternColumn,
|
||||
DateHistogramIndexPatternColumn,
|
||||
SeriesType,
|
||||
OperationType,
|
||||
IndexPatternColumn,
|
||||
} from '../../../../../lens/public';
|
||||
|
||||
import { PersistableFilter } from '../../../../../lens/common';
|
||||
import { IIndexPattern } from '../../../../../../../src/plugins/data/common/index_patterns';
|
||||
|
||||
export const ReportViewTypes = {
|
||||
pld: 'page-load-dist',
|
||||
kpi: 'kpi-trends',
|
||||
upd: 'uptime-duration',
|
||||
upp: 'uptime-pings',
|
||||
svl: 'service-latency',
|
||||
tpt: 'service-throughput',
|
||||
logs: 'logs-frequency',
|
||||
cpu: 'cpu-usage',
|
||||
mem: 'memory-usage',
|
||||
nwk: 'network-activity',
|
||||
} as const;
|
||||
|
||||
type ValueOf<T> = T[keyof T];
|
||||
|
||||
export type ReportViewTypeId = keyof typeof ReportViewTypes;
|
||||
|
||||
export type ReportViewType = ValueOf<typeof ReportViewTypes>;
|
||||
|
||||
export interface ReportDefinition {
|
||||
field: string;
|
||||
required?: boolean;
|
||||
custom?: boolean;
|
||||
defaultValue?: string;
|
||||
options?: Array<{ field: string; label: string; description?: string }>;
|
||||
}
|
||||
|
||||
export interface DataSeries {
|
||||
reportType: ReportViewType;
|
||||
id: string;
|
||||
xAxisColumn: Partial<LastValueIndexPatternColumn> | Partial<DateHistogramIndexPatternColumn>;
|
||||
yAxisColumn: Partial<IndexPatternColumn>;
|
||||
|
||||
breakdowns: string[];
|
||||
defaultSeriesType: SeriesType;
|
||||
defaultFilters: Array<string | { field: string; nested: string }>;
|
||||
seriesTypes: SeriesType[];
|
||||
filters?: PersistableFilter[];
|
||||
reportDefinitions: ReportDefinition[];
|
||||
labels: Record<string, string>;
|
||||
hasMetricType: boolean;
|
||||
palette?: PaletteOutput;
|
||||
}
|
||||
|
||||
export interface SeriesUrl {
|
||||
time: {
|
||||
to: string;
|
||||
from: string;
|
||||
};
|
||||
breakdown?: string;
|
||||
filters?: UrlFilter[];
|
||||
seriesType?: SeriesType;
|
||||
reportType: ReportViewTypeId;
|
||||
metric?: OperationType;
|
||||
dataType?: AppDataType;
|
||||
reportDefinitions?: Record<string, string>;
|
||||
}
|
||||
|
||||
export interface UrlFilter {
|
||||
field: string;
|
||||
values?: string[];
|
||||
notValues?: string[];
|
||||
}
|
||||
|
||||
export interface ConfigProps {
|
||||
seriesId: string;
|
||||
indexPattern: IIndexPattern;
|
||||
}
|
||||
|
||||
export type AppDataType = 'synthetics' | 'rum' | 'logs' | 'metrics' | 'apm';
|
|
@ -15,14 +15,19 @@ import {
|
|||
EuiSelectableOption,
|
||||
} from '@elastic/eui';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { PopoverAnchorPosition } from '@elastic/eui/src/components/popover/popover';
|
||||
|
||||
export interface FieldValueSelectionProps {
|
||||
value?: string;
|
||||
label: string;
|
||||
loading: boolean;
|
||||
loading?: boolean;
|
||||
onChange: (val?: string) => void;
|
||||
values?: string[];
|
||||
setQuery: Dispatch<SetStateAction<string>>;
|
||||
anchorPosition?: PopoverAnchorPosition;
|
||||
forceOpen?: boolean;
|
||||
button?: JSX.Element;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
const formatOptions = (values?: string[], value?: string): EuiSelectableOption[] => {
|
||||
|
@ -38,6 +43,10 @@ export function FieldValueSelection({
|
|||
loading,
|
||||
values,
|
||||
setQuery,
|
||||
button,
|
||||
width,
|
||||
forceOpen,
|
||||
anchorPosition,
|
||||
onChange: onSelectionChange,
|
||||
}: FieldValueSelectionProps) {
|
||||
const [options, setOptions] = useState<EuiSelectableOption[]>(formatOptions(values, value));
|
||||
|
@ -63,8 +72,9 @@ export function FieldValueSelection({
|
|||
setQuery((evt.target as HTMLInputElement).value);
|
||||
};
|
||||
|
||||
const button = (
|
||||
const anchorButton = (
|
||||
<EuiButton
|
||||
style={width ? { width } : {}}
|
||||
size="s"
|
||||
iconType="arrowDown"
|
||||
iconSide="right"
|
||||
|
@ -80,9 +90,10 @@ export function FieldValueSelection({
|
|||
<EuiPopover
|
||||
id="popover"
|
||||
panelPaddingSize="none"
|
||||
button={button}
|
||||
isOpen={isPopoverOpen}
|
||||
button={button || anchorButton}
|
||||
isOpen={isPopoverOpen || forceOpen}
|
||||
closePopover={closePopover}
|
||||
anchorPosition={anchorPosition}
|
||||
>
|
||||
<EuiSelectable
|
||||
searchable
|
||||
|
|
|
@ -8,16 +8,24 @@
|
|||
import React, { useState } from 'react';
|
||||
|
||||
import { useDebounce } from 'react-use';
|
||||
import { PopoverAnchorPosition } from '@elastic/eui/src/components/popover/popover';
|
||||
import { useValuesList } from '../../../hooks/use_values_list';
|
||||
import { IIndexPattern } from '../../../../../../../src/plugins/data/common';
|
||||
import { IndexPattern } from '../../../../../../../src/plugins/data/common';
|
||||
import { FieldValueSelection } from './field_value_selection';
|
||||
import { ESFilter } from '../../../../../../../typings/elasticsearch';
|
||||
|
||||
export interface FieldValueSuggestionsProps {
|
||||
value?: string;
|
||||
label: string;
|
||||
indexPattern: IIndexPattern;
|
||||
indexPattern: IndexPattern;
|
||||
sourceField: string;
|
||||
onChange: (val?: string) => void;
|
||||
filters: ESFilter[];
|
||||
anchorPosition?: PopoverAnchorPosition;
|
||||
time?: { from: string; to: string };
|
||||
forceOpen?: boolean;
|
||||
button?: JSX.Element;
|
||||
width?: number;
|
||||
}
|
||||
|
||||
export function FieldValueSuggestions({
|
||||
|
@ -25,12 +33,18 @@ export function FieldValueSuggestions({
|
|||
label,
|
||||
indexPattern,
|
||||
value,
|
||||
filters,
|
||||
button,
|
||||
time,
|
||||
width,
|
||||
forceOpen,
|
||||
anchorPosition,
|
||||
onChange: onSelectionChange,
|
||||
}: FieldValueSuggestionsProps) {
|
||||
const [query, setQuery] = useState('');
|
||||
const [debouncedValue, setDebouncedValue] = useState('');
|
||||
|
||||
const { values, loading } = useValuesList({ indexPattern, query, sourceField });
|
||||
const { values, loading } = useValuesList({ indexPattern, query, sourceField, filters, time });
|
||||
|
||||
useDebounce(
|
||||
() => {
|
||||
|
@ -48,6 +62,10 @@ export function FieldValueSuggestions({
|
|||
setQuery={setDebouncedValue}
|
||||
loading={loading}
|
||||
value={value}
|
||||
button={button}
|
||||
forceOpen={forceOpen}
|
||||
anchorPosition={anchorPosition}
|
||||
width={width}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
|
|
@ -17,12 +17,19 @@ import { HasData, ObservabilityFetchDataPlugins } from '../typings/fetch_overvie
|
|||
import { HasDataContextProvider } from './has_data_context';
|
||||
import * as pluginContext from '../hooks/use_plugin_context';
|
||||
import { PluginContextValue } from './plugin_context';
|
||||
import { Router } from 'react-router-dom';
|
||||
import { createMemoryHistory } from 'history';
|
||||
|
||||
const relativeStart = '2020-10-08T06:00:00.000Z';
|
||||
const relativeEnd = '2020-10-08T07:00:00.000Z';
|
||||
|
||||
function wrapper({ children }: { children: React.ReactElement }) {
|
||||
return <HasDataContextProvider>{children}</HasDataContextProvider>;
|
||||
const history = createMemoryHistory();
|
||||
return (
|
||||
<Router history={history}>
|
||||
<HasDataContextProvider>{children}</HasDataContextProvider>
|
||||
</Router>
|
||||
);
|
||||
}
|
||||
|
||||
function unregisterAll() {
|
||||
|
|
|
@ -7,6 +7,7 @@
|
|||
|
||||
import { uniqueId } from 'lodash';
|
||||
import React, { createContext, useEffect, useState } from 'react';
|
||||
import { useRouteMatch } from 'react-router-dom';
|
||||
import { Alert } from '../../../alerting/common';
|
||||
import { getDataHandler } from '../data_handler';
|
||||
import { FETCH_STATUS } from '../hooks/use_fetcher';
|
||||
|
@ -41,35 +42,38 @@ export function HasDataContextProvider({ children }: { children: React.ReactNode
|
|||
|
||||
const [hasData, setHasData] = useState<HasDataContextValue['hasData']>({});
|
||||
|
||||
const isExploratoryView = useRouteMatch('/exploratory-view');
|
||||
|
||||
useEffect(
|
||||
() => {
|
||||
apps.forEach(async (app) => {
|
||||
try {
|
||||
if (app !== 'alert') {
|
||||
const params =
|
||||
app === 'ux'
|
||||
? { absoluteTime: { start: absoluteStart, end: absoluteEnd } }
|
||||
: undefined;
|
||||
if (!isExploratoryView)
|
||||
apps.forEach(async (app) => {
|
||||
try {
|
||||
if (app !== 'alert') {
|
||||
const params =
|
||||
app === 'ux'
|
||||
? { absoluteTime: { start: absoluteStart, end: absoluteEnd } }
|
||||
: undefined;
|
||||
|
||||
const result = await getDataHandler(app)?.hasData(params);
|
||||
const result = await getDataHandler(app)?.hasData(params);
|
||||
setHasData((prevState) => ({
|
||||
...prevState,
|
||||
[app]: {
|
||||
hasData: result,
|
||||
status: FETCH_STATUS.SUCCESS,
|
||||
},
|
||||
}));
|
||||
}
|
||||
} catch (e) {
|
||||
setHasData((prevState) => ({
|
||||
...prevState,
|
||||
[app]: {
|
||||
hasData: result,
|
||||
status: FETCH_STATUS.SUCCESS,
|
||||
hasData: undefined,
|
||||
status: FETCH_STATUS.FAILURE,
|
||||
},
|
||||
}));
|
||||
}
|
||||
} catch (e) {
|
||||
setHasData((prevState) => ({
|
||||
...prevState,
|
||||
[app]: {
|
||||
hasData: undefined,
|
||||
status: FETCH_STATUS.FAILURE,
|
||||
},
|
||||
}));
|
||||
}
|
||||
});
|
||||
});
|
||||
},
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||
[]
|
||||
|
|
71
x-pack/plugins/observability/public/hooks/use_breadcrumbs.ts
Normal file
71
x-pack/plugins/observability/public/hooks/use_breadcrumbs.ts
Normal file
|
@ -0,0 +1,71 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { ChromeBreadcrumb } from 'kibana/public';
|
||||
import { i18n } from '@kbn/i18n';
|
||||
import { MouseEvent, useEffect } from 'react';
|
||||
import { EuiBreadcrumb } from '@elastic/eui';
|
||||
import { stringify } from 'query-string';
|
||||
import { useKibana } from '../../../../../src/plugins/kibana_react/public';
|
||||
import { useQueryParams } from './use_query_params';
|
||||
|
||||
const EMPTY_QUERY = '?';
|
||||
|
||||
function handleBreadcrumbClick(
|
||||
breadcrumbs: ChromeBreadcrumb[],
|
||||
navigateToHref?: (url: string) => Promise<void>
|
||||
) {
|
||||
return breadcrumbs.map((bc) => ({
|
||||
...bc,
|
||||
...(bc.href
|
||||
? {
|
||||
onClick: (event: MouseEvent) => {
|
||||
if (navigateToHref && bc.href) {
|
||||
event.preventDefault();
|
||||
navigateToHref(bc.href);
|
||||
}
|
||||
},
|
||||
}
|
||||
: {}),
|
||||
}));
|
||||
}
|
||||
|
||||
export const makeBaseBreadcrumb = (href: string, params?: any): EuiBreadcrumb => {
|
||||
if (params) {
|
||||
const crumbParams = { ...params };
|
||||
|
||||
delete crumbParams.statusFilter;
|
||||
const query = stringify(crumbParams, { skipEmptyString: true, skipNull: true });
|
||||
href += query === EMPTY_QUERY ? '' : query;
|
||||
}
|
||||
return {
|
||||
text: i18n.translate('xpack.observability.breadcrumbs.observability', {
|
||||
defaultMessage: 'Observability',
|
||||
}),
|
||||
href,
|
||||
};
|
||||
};
|
||||
|
||||
export const useBreadcrumbs = (extraCrumbs: ChromeBreadcrumb[]) => {
|
||||
const params = useQueryParams();
|
||||
|
||||
const {
|
||||
services: { chrome, application },
|
||||
} = useKibana();
|
||||
|
||||
const setBreadcrumbs = chrome?.setBreadcrumbs;
|
||||
const appPath = application?.getUrlForApp('observability-overview') ?? '';
|
||||
const navigate = application?.navigateToUrl;
|
||||
|
||||
useEffect(() => {
|
||||
if (setBreadcrumbs) {
|
||||
setBreadcrumbs(
|
||||
handleBreadcrumbClick([makeBaseBreadcrumb(appPath, params)].concat(extraCrumbs), navigate)
|
||||
);
|
||||
}
|
||||
}, [appPath, extraCrumbs, navigate, params, setBreadcrumbs]);
|
||||
};
|
|
@ -0,0 +1,22 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { useUiSetting } from '../../../../../src/plugins/kibana_react/public';
|
||||
import { UI_SETTINGS } from '../../../../../src/plugins/data/common';
|
||||
import { TimePickerQuickRange } from '../components/shared/exploratory_view/series_date_picker';
|
||||
|
||||
export function useQuickTimeRanges() {
|
||||
const timePickerQuickRanges = useUiSetting<TimePickerQuickRange[]>(
|
||||
UI_SETTINGS.TIMEPICKER_QUICK_RANGES
|
||||
);
|
||||
|
||||
return timePickerQuickRanges.map(({ from, to, display }) => ({
|
||||
start: from,
|
||||
end: to,
|
||||
label: display,
|
||||
}));
|
||||
}
|
|
@ -5,32 +5,58 @@
|
|||
* 2.0.
|
||||
*/
|
||||
|
||||
import { IIndexPattern } from '../../../../../src/plugins/data/common';
|
||||
import { IndexPattern } from '../../../../../src/plugins/data/common';
|
||||
import { useKibana } from '../../../../../src/plugins/kibana_react/public';
|
||||
import { DataPublicPluginStart } from '../../../../../src/plugins/data/public';
|
||||
import { useFetcher } from './use_fetcher';
|
||||
import { ESFilter } from '../../../../../typings/elasticsearch';
|
||||
import { DataPublicPluginStart } from '../../../../../src/plugins/data/public';
|
||||
|
||||
interface Props {
|
||||
export interface Props {
|
||||
sourceField: string;
|
||||
query?: string;
|
||||
indexPattern: IIndexPattern;
|
||||
indexPattern: IndexPattern;
|
||||
filters?: ESFilter[];
|
||||
time?: { from: string; to: string };
|
||||
}
|
||||
|
||||
export const useValuesList = ({ sourceField, indexPattern, query, filters }: Props) => {
|
||||
export const useValuesList = ({
|
||||
sourceField,
|
||||
indexPattern,
|
||||
query = '',
|
||||
filters,
|
||||
time,
|
||||
}: Props): { values: string[]; loading?: boolean } => {
|
||||
const {
|
||||
services: { data },
|
||||
} = useKibana<{ data: DataPublicPluginStart }>();
|
||||
|
||||
const { data: values, status } = useFetcher(() => {
|
||||
const { from, to } = time ?? {};
|
||||
|
||||
const { data: values, loading } = useFetcher(() => {
|
||||
if (!sourceField || !indexPattern) {
|
||||
return [];
|
||||
}
|
||||
return data.autocomplete.getValueSuggestions({
|
||||
indexPattern,
|
||||
query: query || '',
|
||||
field: indexPattern.fields.find(({ name }) => name === sourceField)!,
|
||||
boolFilter: filters ?? [],
|
||||
useTimeRange: !(from && to),
|
||||
field: indexPattern.getFieldByName(sourceField)!,
|
||||
boolFilter:
|
||||
from && to
|
||||
? [
|
||||
...(filters || []),
|
||||
{
|
||||
range: {
|
||||
'@timestamp': {
|
||||
gte: from,
|
||||
lte: to,
|
||||
},
|
||||
},
|
||||
},
|
||||
]
|
||||
: filters || [],
|
||||
});
|
||||
}, [sourceField, query, data.autocomplete, indexPattern, filters]);
|
||||
}, [query, sourceField, data.autocomplete, indexPattern, from, to, filters]);
|
||||
|
||||
return { values, loading: status === 'loading' || status === 'pending' };
|
||||
return { values: values as string[], loading };
|
||||
};
|
||||
|
|
|
@ -55,3 +55,4 @@ export * from './typings';
|
|||
export { useChartTheme } from './hooks/use_chart_theme';
|
||||
export { useTheme } from './hooks/use_theme';
|
||||
export { getApmTraceUrl } from './utils/get_apm_trace_url';
|
||||
export { createExploratoryViewUrl } from './components/shared/exploratory_view/configurations/utils';
|
||||
|
|
|
@ -14,6 +14,7 @@ import { OverviewPage } from '../pages/overview';
|
|||
import { jsonRt } from './json_rt';
|
||||
import { AlertsPage } from '../pages/alerts';
|
||||
import { CasesPage } from '../pages/cases';
|
||||
import { ExploratoryViewPage } from '../components/shared/exploratory_view';
|
||||
|
||||
export type RouteParams<T extends keyof typeof routes> = DecodeParams<typeof routes[T]['params']>;
|
||||
|
||||
|
@ -115,4 +116,24 @@ export const routes = {
|
|||
},
|
||||
],
|
||||
},
|
||||
'/exploratory-view': {
|
||||
handler: () => {
|
||||
return <ExploratoryViewPage />;
|
||||
},
|
||||
params: {
|
||||
query: t.partial({
|
||||
rangeFrom: t.string,
|
||||
rangeTo: t.string,
|
||||
refreshPaused: jsonRt.pipe(t.boolean),
|
||||
refreshInterval: jsonRt.pipe(t.number),
|
||||
}),
|
||||
},
|
||||
breadcrumb: [
|
||||
{
|
||||
text: i18n.translate('xpack.observability.overview.exploratoryView', {
|
||||
defaultMessage: 'Exploratory view',
|
||||
}),
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
/*
|
||||
* Copyright Elasticsearch B.V. and/or licensed to Elasticsearch B.V. under one
|
||||
* or more contributor license agreements. Licensed under the Elastic License
|
||||
* 2.0; you may not use this file except in compliance with the Elastic License
|
||||
* 2.0.
|
||||
*/
|
||||
|
||||
import { DataPublicPluginStart, IndexPattern } from '../../../../../src/plugins/data/public';
|
||||
|
||||
export type DataType = 'synthetics' | 'apm' | 'logs' | 'metrics' | 'rum';
|
||||
|
||||
const indexPatternList: Record<DataType, string> = {
|
||||
synthetics: 'synthetics_static_index_pattern_id',
|
||||
apm: 'apm_static_index_pattern_id',
|
||||
rum: 'apm_static_index_pattern_id',
|
||||
logs: 'logs_static_index_pattern_id',
|
||||
metrics: 'metrics_static_index_pattern_id',
|
||||
};
|
||||
|
||||
const appToPatternMap: Record<DataType, string> = {
|
||||
synthetics: 'heartbeat-*',
|
||||
apm: 'apm-*',
|
||||
rum: 'apm-*',
|
||||
logs: 'logs-*,filebeat-*',
|
||||
metrics: 'metrics-*,metricbeat-*',
|
||||
};
|
||||
|
||||
export class ObservabilityIndexPatterns {
|
||||
data?: DataPublicPluginStart;
|
||||
|
||||
constructor(data: DataPublicPluginStart) {
|
||||
this.data = data;
|
||||
}
|
||||
|
||||
async createIndexPattern(app: DataType) {
|
||||
if (!this.data) {
|
||||
throw new Error('data is not defined');
|
||||
}
|
||||
|
||||
const pattern = appToPatternMap[app];
|
||||
|
||||
const fields = await this.data.indexPatterns.getFieldsForWildcard({
|
||||
pattern,
|
||||
});
|
||||
|
||||
return await this.data.indexPatterns.createAndSave({
|
||||
fields,
|
||||
title: pattern,
|
||||
id: indexPatternList[app],
|
||||
timeFieldName: '@timestamp',
|
||||
});
|
||||
}
|
||||
|
||||
async getIndexPattern(app: DataType): Promise<IndexPattern | undefined> {
|
||||
if (!this.data) {
|
||||
throw new Error('data is not defined');
|
||||
}
|
||||
try {
|
||||
return await this.data?.indexPatterns.get(indexPatternList[app]);
|
||||
} catch (e) {
|
||||
return await this.createIndexPattern(app || 'apm');
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,7 +7,14 @@
|
|||
"declaration": true,
|
||||
"declarationMap": true
|
||||
},
|
||||
"include": ["common/**/*", "public/**/*", "server/**/*", "typings/**/*"],
|
||||
"include": [
|
||||
"common/**/*",
|
||||
"public/**/*",
|
||||
"public/**/*.json",
|
||||
"server/**/*",
|
||||
"typings/**/*",
|
||||
"../../../typings/**/*"
|
||||
],
|
||||
"references": [
|
||||
{ "path": "../../../src/core/tsconfig.json" },
|
||||
{ "path": "../../../src/plugins/data/tsconfig.json" },
|
||||
|
|
|
@ -68,18 +68,21 @@ export class UptimePlugin
|
|||
|
||||
return UptimeDataHelper(coreStart);
|
||||
};
|
||||
plugins.observability.dashboard.register({
|
||||
appName: 'uptime',
|
||||
hasData: async () => {
|
||||
const dataHelper = await getUptimeDataHelper();
|
||||
const status = await dataHelper.indexStatus();
|
||||
return status.docCount > 0;
|
||||
},
|
||||
fetchData: async (params: FetchDataParams) => {
|
||||
const dataHelper = await getUptimeDataHelper();
|
||||
return await dataHelper.overviewData(params);
|
||||
},
|
||||
});
|
||||
|
||||
if (plugins.observability) {
|
||||
plugins.observability.dashboard.register({
|
||||
appName: 'uptime',
|
||||
hasData: async () => {
|
||||
const dataHelper = await getUptimeDataHelper();
|
||||
const status = await dataHelper.indexStatus();
|
||||
return status.docCount > 0;
|
||||
},
|
||||
fetchData: async (params: FetchDataParams) => {
|
||||
const dataHelper = await getUptimeDataHelper();
|
||||
return await dataHelper.overviewData(params);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
core.application.register({
|
||||
id: PLUGIN.ID,
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue