mirror of https://github.com/apache/druid.git
Druid Doctor (#8672)
* adding Druid doctor * better meesage * feedback changes * add icons * feedback fixes * spelling * add file.encoding check * feedback changes
This commit is contained in:
parent
a8b674e00d
commit
d9c9aef3d1
|
@ -839,16 +839,17 @@
|
|||
}
|
||||
},
|
||||
"@blueprintjs/core": {
|
||||
"version": "3.18.0",
|
||||
"resolved": "https://registry.npmjs.org/@blueprintjs/core/-/core-3.18.0.tgz",
|
||||
"integrity": "sha512-dr3A6uhpAAWmf5muY6PFQp5EgEzinRLZa/TGQMA05q1P2xrOn/LYlsqJWJBUJ3j9tmh1RP8VPeu1HmosQb3J7w==",
|
||||
"version": "3.19.1",
|
||||
"resolved": "https://registry.npmjs.org/@blueprintjs/core/-/core-3.19.1.tgz",
|
||||
"integrity": "sha512-O+p/Jbu9kyrTE4Yy5phd7Xp9NJoiHCQ9ycO+QSR2fiuhwnVMO1xjY269QXxylzEswgPyjhST1euqHpyQQ7qnUw==",
|
||||
"requires": {
|
||||
"@blueprintjs/icons": "^3.10.0",
|
||||
"@blueprintjs/icons": "^3.11.0",
|
||||
"@types/dom4": "^2.0.1",
|
||||
"classnames": "^2.2",
|
||||
"dom4": "^2.1.5",
|
||||
"normalize.css": "^8.0.1",
|
||||
"popper.js": "^1.15.0",
|
||||
"react-lifecycles-compat": "^3.0.4",
|
||||
"react-popper": "^1.3.3",
|
||||
"react-transition-group": "^2.9.0",
|
||||
"resize-observer-polyfill": "^1.5.1",
|
||||
|
@ -863,9 +864,9 @@
|
|||
}
|
||||
},
|
||||
"@blueprintjs/icons": {
|
||||
"version": "3.10.0",
|
||||
"resolved": "https://registry.npmjs.org/@blueprintjs/icons/-/icons-3.10.0.tgz",
|
||||
"integrity": "sha512-lyAUpkr3qEStPcJpMnxRKuVAPvaRNSce1ySPbkE58zPmD4WBya2gNrWex41xoqRYM0GsiBSwH9CnpY8t6fZKUA==",
|
||||
"version": "3.11.0",
|
||||
"resolved": "https://registry.npmjs.org/@blueprintjs/icons/-/icons-3.11.0.tgz",
|
||||
"integrity": "sha512-HGS652gFc057t9cr8NyuWFyZ1gcSqG3uuexpzhZm81W35hGfh9vdC9GR+mbHJNawAuKXtu+xw4VWWkv1UGQ0Vg==",
|
||||
"requires": {
|
||||
"classnames": "^2.2",
|
||||
"tslib": "~1.9.0"
|
||||
|
@ -2243,7 +2244,8 @@
|
|||
"asap": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
|
||||
"integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY="
|
||||
"integrity": "sha1-5QNHYR1+aQlDIIu9r+vLwvuGbUY=",
|
||||
"dev": true
|
||||
},
|
||||
"asn1": {
|
||||
"version": "0.2.4",
|
||||
|
@ -3556,12 +3558,12 @@
|
|||
}
|
||||
},
|
||||
"create-react-context": {
|
||||
"version": "0.2.2",
|
||||
"resolved": "https://registry.npmjs.org/create-react-context/-/create-react-context-0.2.2.tgz",
|
||||
"integrity": "sha512-KkpaLARMhsTsgp0d2NA/R94F/eDLbhXERdIq3LvX2biCAXcDvHYoOqHfWCHf1+OLj+HKBotLG3KqaOOf+C1C+A==",
|
||||
"version": "0.3.0",
|
||||
"resolved": "https://registry.npmjs.org/create-react-context/-/create-react-context-0.3.0.tgz",
|
||||
"integrity": "sha512-dNldIoSuNSvlTJ7slIKC/ZFGKexBMBrrcc+TTe1NdmROnaASuLPvqpwj9v4XS4uXZ8+YPu0sNmShX2rXI5LNsw==",
|
||||
"requires": {
|
||||
"fbjs": "^0.8.0",
|
||||
"gud": "^1.0.0"
|
||||
"gud": "^1.0.0",
|
||||
"warning": "^4.0.3"
|
||||
}
|
||||
},
|
||||
"cross-spawn": {
|
||||
|
@ -4574,14 +4576,6 @@
|
|||
"integrity": "sha1-rT/0yG7C0CkyL1oCw6mmBslbP1k=",
|
||||
"dev": true
|
||||
},
|
||||
"encoding": {
|
||||
"version": "0.1.12",
|
||||
"resolved": "https://registry.npmjs.org/encoding/-/encoding-0.1.12.tgz",
|
||||
"integrity": "sha1-U4tm8+5izRq1HsMjgp0flIDHS+s=",
|
||||
"requires": {
|
||||
"iconv-lite": "~0.4.13"
|
||||
}
|
||||
},
|
||||
"end-of-stream": {
|
||||
"version": "1.4.1",
|
||||
"resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.1.tgz",
|
||||
|
@ -5176,27 +5170,6 @@
|
|||
"bser": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"fbjs": {
|
||||
"version": "0.8.17",
|
||||
"resolved": "https://registry.npmjs.org/fbjs/-/fbjs-0.8.17.tgz",
|
||||
"integrity": "sha1-xNWY6taUkRJlPWWIsBpc3Nn5D90=",
|
||||
"requires": {
|
||||
"core-js": "^1.0.0",
|
||||
"isomorphic-fetch": "^2.1.1",
|
||||
"loose-envify": "^1.0.0",
|
||||
"object-assign": "^4.1.0",
|
||||
"promise": "^7.1.1",
|
||||
"setimmediate": "^1.0.5",
|
||||
"ua-parser-js": "^0.7.18"
|
||||
},
|
||||
"dependencies": {
|
||||
"core-js": {
|
||||
"version": "1.2.7",
|
||||
"resolved": "https://registry.npmjs.org/core-js/-/core-js-1.2.7.tgz",
|
||||
"integrity": "sha1-ZSKUwUZR2yj6k70tX/KYOk8IxjY="
|
||||
}
|
||||
}
|
||||
},
|
||||
"figgy-pudding": {
|
||||
"version": "3.5.1",
|
||||
"resolved": "https://registry.npmjs.org/figgy-pudding/-/figgy-pudding-3.5.1.tgz",
|
||||
|
@ -7245,7 +7218,8 @@
|
|||
"is-stream": {
|
||||
"version": "1.1.0",
|
||||
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-1.1.0.tgz",
|
||||
"integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ="
|
||||
"integrity": "sha1-EtSj3U5o4Lec6428hBc66A2RykQ=",
|
||||
"dev": true
|
||||
},
|
||||
"is-string": {
|
||||
"version": "1.0.4",
|
||||
|
@ -7321,15 +7295,6 @@
|
|||
"integrity": "sha1-TkMekrEalzFjaqH5yNHMvP2reN8=",
|
||||
"dev": true
|
||||
},
|
||||
"isomorphic-fetch": {
|
||||
"version": "2.2.1",
|
||||
"resolved": "https://registry.npmjs.org/isomorphic-fetch/-/isomorphic-fetch-2.2.1.tgz",
|
||||
"integrity": "sha1-YRrhrPFPXoH3KVB0coGf6XM1WKk=",
|
||||
"requires": {
|
||||
"node-fetch": "^1.0.1",
|
||||
"whatwg-fetch": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"isstream": {
|
||||
"version": "0.1.2",
|
||||
"resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz",
|
||||
|
@ -8970,15 +8935,6 @@
|
|||
"integrity": "sha512-1nh45deeb5olNY7eX82BkPO7SSxR5SSYJiPTrTdFUVYwAl8CKMA5N9PjTYkHiRjisVcxcQ1HXdLhx2qxxJzLNQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node-fetch": {
|
||||
"version": "1.7.3",
|
||||
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-1.7.3.tgz",
|
||||
"integrity": "sha512-NhZ4CsKx7cYm2vSrBAr2PvFOe6sWDf0UYLRqA6svUYg7+/TSfVAu49jYC4BvQ4Sms9SZgdqGBgroqfDhJdTyKQ==",
|
||||
"requires": {
|
||||
"encoding": "^0.1.11",
|
||||
"is-stream": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"node-forge": {
|
||||
"version": "0.7.5",
|
||||
"resolved": "https://registry.npmjs.org/node-forge/-/node-forge-0.7.5.tgz",
|
||||
|
@ -10691,14 +10647,6 @@
|
|||
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==",
|
||||
"dev": true
|
||||
},
|
||||
"promise": {
|
||||
"version": "7.3.1",
|
||||
"resolved": "https://registry.npmjs.org/promise/-/promise-7.3.1.tgz",
|
||||
"integrity": "sha512-nolQXZ/4L+bP/UGlkfaIujX9BKxGwmQ9OT4mOt5yvy8iK1h3wqTEJCijzGANTCCl9nWjY41juyAn2K3Q1hLLTg==",
|
||||
"requires": {
|
||||
"asap": "~2.0.3"
|
||||
}
|
||||
},
|
||||
"promise-inflight": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/promise-inflight/-/promise-inflight-1.0.1.tgz",
|
||||
|
@ -10965,12 +10913,12 @@
|
|||
"integrity": "sha512-fBASbA6LnOU9dOU2eW7aQ8xmYBSXUIWr+UmF9b1efZBazGNO+rcXT/icdKnYm2pTwcRylVUYwW7H1PHfLekVzA=="
|
||||
},
|
||||
"react-popper": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.3.tgz",
|
||||
"integrity": "sha512-ynMZBPkXONPc5K4P5yFWgZx5JGAUIP3pGGLNs58cfAPgK67olx7fmLp+AdpZ0+GoQ+ieFDa/z4cdV6u7sioH6w==",
|
||||
"version": "1.3.4",
|
||||
"resolved": "https://registry.npmjs.org/react-popper/-/react-popper-1.3.4.tgz",
|
||||
"integrity": "sha512-9AcQB29V+WrBKk6X7p0eojd1f25/oJajVdMZkywIoAV6Ag7hzE1Mhyeup2Q1QnvFRtGQFQvtqfhlEoDAPfKAVA==",
|
||||
"requires": {
|
||||
"@babel/runtime": "^7.1.2",
|
||||
"create-react-context": "<=0.2.2",
|
||||
"create-react-context": "^0.3.0",
|
||||
"popper.js": "^1.14.4",
|
||||
"prop-types": "^15.6.1",
|
||||
"typed-styles": "^0.0.7",
|
||||
|
@ -12099,7 +12047,8 @@
|
|||
"setimmediate": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz",
|
||||
"integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU="
|
||||
"integrity": "sha1-KQy7Iy4waULX1+qbg3Mqt4VvgoU=",
|
||||
"dev": true
|
||||
},
|
||||
"setprototypeof": {
|
||||
"version": "1.1.1",
|
||||
|
@ -13931,11 +13880,6 @@
|
|||
"integrity": "sha512-ACzBtm/PhXBDId6a6sDJfroT2pOWt/oOnk4/dElG5G33ZL776N3Y6/6bKZJBFpd+b05F3Ct9qDjMeJmRWtE2/g==",
|
||||
"dev": true
|
||||
},
|
||||
"ua-parser-js": {
|
||||
"version": "0.7.20",
|
||||
"resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.20.tgz",
|
||||
"integrity": "sha512-8OaIKfzL5cpx8eCMAhhvTlft8GYF8b2eQr6JkCyVdrgjcytyOmPCXrqXFcUnhonRpLlh5yxEZVohm6mzaowUOw=="
|
||||
},
|
||||
"uglify-js": {
|
||||
"version": "3.6.0",
|
||||
"resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.6.0.tgz",
|
||||
|
@ -14767,11 +14711,6 @@
|
|||
"iconv-lite": "0.4.24"
|
||||
}
|
||||
},
|
||||
"whatwg-fetch": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-fetch/-/whatwg-fetch-3.0.0.tgz",
|
||||
"integrity": "sha512-9GSJUgz1D4MfyKU7KRqwOjXCXTqWdFNvEr7eUBYchQiVc744mqK/MzXPNR2WsPkmkOa4ywfg8C2n8h+13Bey1Q=="
|
||||
},
|
||||
"whatwg-mimetype": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/whatwg-mimetype/-/whatwg-mimetype-2.3.0.tgz",
|
||||
|
|
|
@ -53,8 +53,8 @@
|
|||
"start": "webpack-dev-server --hot --open"
|
||||
},
|
||||
"dependencies": {
|
||||
"@blueprintjs/core": "^3.18.0",
|
||||
"@blueprintjs/icons": "^3.10.0",
|
||||
"@blueprintjs/core": "^3.19.1",
|
||||
"@blueprintjs/icons": "^3.11.0",
|
||||
"axios": "^0.19.0",
|
||||
"brace": "^0.11.1",
|
||||
"classnames": "^2.2.6",
|
||||
|
|
|
@ -94,6 +94,15 @@ exports[`header bar matches snapshot 1`] = `
|
|||
captureDismiss={false}
|
||||
content={
|
||||
<Blueprint3.Menu>
|
||||
<Blueprint3.MenuItem
|
||||
disabled={false}
|
||||
icon="pulse"
|
||||
multiline={false}
|
||||
onClick={[Function]}
|
||||
popoverProps={Object {}}
|
||||
shouldDismissPopover={true}
|
||||
text="Druid Doctor"
|
||||
/>
|
||||
<Blueprint3.MenuItem
|
||||
disabled={false}
|
||||
icon="settings"
|
||||
|
|
|
@ -34,6 +34,7 @@ import React, { useState } from 'react';
|
|||
|
||||
import { AboutDialog } from '../../dialogs/about-dialog/about-dialog';
|
||||
import { CoordinatorDynamicConfigDialog } from '../../dialogs/coordinator-dynamic-config-dialog/coordinator-dynamic-config-dialog';
|
||||
import { DoctorDialog } from '../../dialogs/doctor-dialog/doctor-dialog';
|
||||
import { OverlordDynamicConfigDialog } from '../../dialogs/overlord-dynamic-config-dialog/overlord-dynamic-config-dialog';
|
||||
import {
|
||||
DRUID_ASF_SLACK,
|
||||
|
@ -134,6 +135,7 @@ export interface HeaderBarProps {
|
|||
export function HeaderBar(props: HeaderBarProps) {
|
||||
const { active, hideLegacy } = props;
|
||||
const [aboutDialogOpen, setAboutDialogOpen] = useState(false);
|
||||
const [doctorDialogOpen, setDoctorDialogOpen] = useState(false);
|
||||
const [coordinatorDynamicConfigDialogOpen, setCoordinatorDynamicConfigDialogOpen] = useState(
|
||||
false,
|
||||
);
|
||||
|
@ -157,6 +159,11 @@ export function HeaderBar(props: HeaderBarProps) {
|
|||
|
||||
const configMenu = (
|
||||
<Menu>
|
||||
<MenuItem
|
||||
icon={IconNames.PULSE}
|
||||
text="Druid Doctor"
|
||||
onClick={() => setDoctorDialogOpen(true)}
|
||||
/>
|
||||
<MenuItem
|
||||
icon={IconNames.SETTINGS}
|
||||
text="Coordinator dynamic config"
|
||||
|
@ -246,6 +253,7 @@ export function HeaderBar(props: HeaderBarProps) {
|
|||
</Popover>
|
||||
</NavbarGroup>
|
||||
{aboutDialogOpen && <AboutDialog onClose={() => setAboutDialogOpen(false)} />}
|
||||
{doctorDialogOpen && <DoctorDialog onClose={() => setDoctorDialogOpen(false)} />}
|
||||
{coordinatorDynamicConfigDialogOpen && (
|
||||
<CoordinatorDynamicConfigDialog
|
||||
onClose={() => setCoordinatorDynamicConfigDialogOpen(false)}
|
||||
|
|
|
@ -16,7 +16,7 @@ exports[`about dialog matches snapshot 1`] = `
|
|||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="bp3-dialog"
|
||||
class="bp3-dialog about-dialog"
|
||||
>
|
||||
<div
|
||||
class="bp3-dialog-header"
|
||||
|
|
|
@ -36,7 +36,14 @@ export function AboutDialog(props: AboutDialogProps) {
|
|||
const { onClose } = props;
|
||||
|
||||
return (
|
||||
<Dialog icon={IconNames.GRAPH} onClose={onClose} title="Apache Druid" isOpen canEscapeKeyClose>
|
||||
<Dialog
|
||||
className="about-dialog"
|
||||
icon={IconNames.GRAPH}
|
||||
onClose={onClose}
|
||||
title="Apache Druid"
|
||||
isOpen
|
||||
canEscapeKeyClose
|
||||
>
|
||||
<div className={Classes.DIALOG_BODY}>
|
||||
<p>
|
||||
<strong>
|
||||
|
|
|
@ -0,0 +1,118 @@
|
|||
// Jest Snapshot v1, https://goo.gl/fbAQLP
|
||||
|
||||
exports[`doctor dialog matches snapshot 1`] = `
|
||||
<div
|
||||
class="bp3-portal"
|
||||
>
|
||||
<div
|
||||
class="bp3-overlay bp3-overlay-open bp3-overlay-scroll-container"
|
||||
>
|
||||
<div
|
||||
class="bp3-overlay-backdrop bp3-overlay-appear bp3-overlay-appear-active"
|
||||
/>
|
||||
<div
|
||||
class="bp3-dialog-container bp3-overlay-content bp3-overlay-appear bp3-overlay-appear-active"
|
||||
tabindex="0"
|
||||
>
|
||||
<div
|
||||
class="bp3-dialog doctor-dialog"
|
||||
>
|
||||
<div
|
||||
class="bp3-dialog-header"
|
||||
>
|
||||
<span
|
||||
class="bp3-icon bp3-icon-pulse"
|
||||
icon="pulse"
|
||||
>
|
||||
<svg
|
||||
data-icon="pulse"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
width="20"
|
||||
>
|
||||
<desc>
|
||||
pulse
|
||||
</desc>
|
||||
<path
|
||||
d="M19 10h-2.38L14.9 6.55h-.01c-.17-.32-.5-.55-.89-.55-.43 0-.79.28-.93.66h-.01l-2.75 7.57L7.98 1.82h-.02A.978.978 0 007 1c-.44 0-.8.29-.94.69h-.01L3.28 10H1c-.55 0-1 .45-1 1s.45 1 1 1h3c.44 0 .8-.29.94-.69h.01l1.78-5.34 2.29 12.21h.02c.08.46.47.82.96.82.43 0 .79-.28.93-.66h.01l3.21-8.82.96 1.92h.01c.16.33.49.56.88.56h3c.55 0 1-.45 1-1s-.45-1-1-1z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<h4
|
||||
class="bp3-heading"
|
||||
>
|
||||
Druid Doctor
|
||||
</h4>
|
||||
<button
|
||||
aria-label="Close"
|
||||
class="bp3-button bp3-minimal bp3-dialog-close-button"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="bp3-icon bp3-icon-small-cross"
|
||||
icon="small-cross"
|
||||
>
|
||||
<svg
|
||||
data-icon="small-cross"
|
||||
height="20"
|
||||
viewBox="0 0 20 20"
|
||||
width="20"
|
||||
>
|
||||
<desc>
|
||||
small-cross
|
||||
</desc>
|
||||
<path
|
||||
d="M11.41 10l3.29-3.29c.19-.18.3-.43.3-.71a1.003 1.003 0 00-1.71-.71L10 8.59l-3.29-3.3a1.003 1.003 0 00-1.42 1.42L8.59 10 5.3 13.29c-.19.18-.3.43-.3.71a1.003 1.003 0 001.71.71l3.29-3.3 3.29 3.29c.18.19.43.3.71.3a1.003 1.003 0 00.71-1.71L11.41 10z"
|
||||
fill-rule="evenodd"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="bp3-dialog-body"
|
||||
>
|
||||
<div
|
||||
class="analyze-bar"
|
||||
>
|
||||
<div
|
||||
class="bp3-callout diagnosis"
|
||||
>
|
||||
Automated checks to troubleshoot issues with the cluster.
|
||||
</div>
|
||||
<button
|
||||
class="bp3-button bp3-fill bp3-intent-primary"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="bp3-button-text"
|
||||
>
|
||||
Analyze Druid cluster
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<div
|
||||
class="bp3-dialog-footer"
|
||||
>
|
||||
<div
|
||||
class="bp3-dialog-footer-actions"
|
||||
>
|
||||
<button
|
||||
class="bp3-button"
|
||||
type="button"
|
||||
>
|
||||
<span
|
||||
class="bp3-button-text"
|
||||
>
|
||||
Close
|
||||
</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
|
@ -0,0 +1,423 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import axios from 'axios';
|
||||
|
||||
import { pluralIfNeeded, queryDruidSql } from '../../utils';
|
||||
import { deepGet } from '../../utils/object-change';
|
||||
import { postToSampler } from '../../utils/sampler';
|
||||
|
||||
export interface CheckControls {
|
||||
addSuggestion: (message: string) => void;
|
||||
addIssue: (message: string) => void;
|
||||
terminateChecks: () => void;
|
||||
}
|
||||
|
||||
export interface DoctorCheck {
|
||||
name: string;
|
||||
check: (controls: CheckControls) => Promise<void>;
|
||||
}
|
||||
|
||||
const RUNTIME_PROPERTIES_ALL_NODES_MUST_AGREE_ON: string[] = [
|
||||
'user.timezone',
|
||||
'druid.zk.service.host',
|
||||
];
|
||||
|
||||
// In the future (when we can query other nodes) is will also be cool to check:
|
||||
// 'druid.storage.type' <=> historicals, overlords, mm
|
||||
// 'druid.indexer.logs.type' <=> overlord, mm, + peons
|
||||
|
||||
const RUNTIME_PROPERTIES_MASTER_NODES_SHOULD_AGREE_ON: string[] = [
|
||||
'druid.metadata.storage.type', // overlord + coordinator
|
||||
'druid.metadata.storage.connector.connectURI',
|
||||
];
|
||||
|
||||
export const DOCTOR_CHECKS: DoctorCheck[] = [
|
||||
// -------------------------------------
|
||||
// Self (router) checks
|
||||
// -------------------------------------
|
||||
{
|
||||
name: 'Verify own status',
|
||||
check: async controls => {
|
||||
// Make sure that the router responds to /status and gives some valid info back
|
||||
let status: any;
|
||||
try {
|
||||
status = (await axios.get(`/status`)).data;
|
||||
} catch (e) {
|
||||
controls.addIssue(
|
||||
`Did not get a /status response from the Router node. Try confirming that it is running and accessible. Got: ${e.message}`,
|
||||
);
|
||||
controls.terminateChecks();
|
||||
return;
|
||||
}
|
||||
|
||||
if (typeof status.version !== 'string') {
|
||||
controls.addIssue('Could not get a valid /status response from the Router.');
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Verify own runtime properties',
|
||||
check: async controls => {
|
||||
// Make sure that everything in /status/properties is above board
|
||||
let properties: Record<string, string>;
|
||||
try {
|
||||
properties = (await axios.get(`/status/properties`)).data;
|
||||
} catch (e) {
|
||||
controls.addIssue(
|
||||
`Did not get a /status/properties response from the Router. Message: ${e.message}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Check that the management proxy is on, it really should be for someone to access the console in the first place but everything could happen
|
||||
if (properties['druid.router.managementProxy.enabled'] !== 'true') {
|
||||
controls.addIssue(
|
||||
`The Router's "druid.router.managementProxy.enabled" is not reported as "true". This means that the Coordinator and Overlord will not be accessible from the Router (and this console).`,
|
||||
);
|
||||
}
|
||||
|
||||
// Check that the underlying Java is Java 8 the only officially supported Java version at the moment.
|
||||
if (
|
||||
properties['java.specification.version'] &&
|
||||
properties['java.specification.version'] !== '1.8'
|
||||
) {
|
||||
controls.addSuggestion(
|
||||
`It looks like are running Java ${properties['java.runtime.version']}. Druid only officially supports Java 1.8.x`,
|
||||
);
|
||||
}
|
||||
|
||||
// Check "file.encoding"
|
||||
if (properties['file.encoding'] && properties['file.encoding'] !== 'UTF-8') {
|
||||
controls.addSuggestion(
|
||||
`It looks like "file.encoding" is set to ${properties['file.encoding']}, it is recommended to set this to "UTF-8"`,
|
||||
);
|
||||
}
|
||||
|
||||
// Check "user.timezone"
|
||||
if (properties['user.timezone'] && properties['user.timezone'] !== 'UTC') {
|
||||
controls.addSuggestion(
|
||||
`It looks like "user.timezone" is set to ${properties['user.timezone']}, it is recommended to set this to "UTC"`,
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
// -------------------------------------
|
||||
// Coordinator and Overlord
|
||||
// -------------------------------------
|
||||
{
|
||||
name: 'Verify the Coordinator and Overlord status',
|
||||
check: async controls => {
|
||||
// Make sure that everything in Coordinator's /status is good
|
||||
let myStatus: any;
|
||||
try {
|
||||
myStatus = (await axios.get(`/status`)).data;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
let coordinatorStatus: any;
|
||||
try {
|
||||
coordinatorStatus = (await axios.get(`/proxy/coordinator/status`)).data;
|
||||
} catch (e) {
|
||||
controls.addIssue(
|
||||
'Did not get a /status response from the Coordinator node. Try confirming that it is running and accessible.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let overlordStatus: any;
|
||||
try {
|
||||
overlordStatus = (await axios.get(`/proxy/overlord/status`)).data;
|
||||
} catch (e) {
|
||||
controls.addIssue(
|
||||
'Did not get a /status response from the Overlord node. Try confirming that it is running and accessible.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (myStatus.version !== coordinatorStatus.version) {
|
||||
controls.addSuggestion(
|
||||
`It looks like the Router and Coordinator nodes are on different versions of Druid. This may indicate a problem if you are not in the middle of a rolling upgrade.`,
|
||||
);
|
||||
}
|
||||
|
||||
if (myStatus.version !== overlordStatus.version) {
|
||||
controls.addSuggestion(
|
||||
`It looks like the Router and Overlord nodes are on different versions of Druid. This may indicate a problem if you are not in the middle of a rolling upgrade.`,
|
||||
);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Verify the Coordinator and Overlord runtime properties',
|
||||
check: async controls => {
|
||||
// Make sure that everything in coordinator and overlord /status/properties is good and matches where needed
|
||||
let myProperties: Record<string, string>;
|
||||
try {
|
||||
myProperties = (await axios.get(`/status/properties`)).data;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
let coordinatorProperties: Record<string, string>;
|
||||
try {
|
||||
coordinatorProperties = (await axios.get(`/proxy/coordinator/status/properties`)).data;
|
||||
} catch (e) {
|
||||
controls.addIssue(
|
||||
'Did not get a /status response from the coordinator. Try confirming that it is running and accessible.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
let overlordProperties: Record<string, string>;
|
||||
try {
|
||||
overlordProperties = (await axios.get(`/proxy/overlord/status/properties`)).data;
|
||||
} catch (e) {
|
||||
controls.addIssue(
|
||||
'Did not get a /status response from the overlord. Try confirming that it is running and accessible.',
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const prop of RUNTIME_PROPERTIES_ALL_NODES_MUST_AGREE_ON) {
|
||||
if (myProperties[prop] !== coordinatorProperties[prop]) {
|
||||
controls.addIssue(
|
||||
`The Router and Coordinator do not agree on the "${prop}" runtime property ("${myProperties[prop]}" vs "${coordinatorProperties[prop]}")`,
|
||||
);
|
||||
}
|
||||
if (myProperties[prop] !== overlordProperties[prop]) {
|
||||
controls.addIssue(
|
||||
`The Router and Overlord do not agree on the "${prop}" runtime property ("${myProperties[prop]}" vs "${overlordProperties[prop]}")`,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
for (const prop of RUNTIME_PROPERTIES_MASTER_NODES_SHOULD_AGREE_ON) {
|
||||
if (coordinatorProperties[prop] !== overlordProperties[prop]) {
|
||||
controls.addSuggestion(
|
||||
`The Coordinator and Overlord do not agree on the "${prop}" runtime property ("${coordinatorProperties[prop]}" vs "${overlordProperties[prop]}")`,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
// -------------------------------------
|
||||
// Check sampler
|
||||
// -------------------------------------
|
||||
{
|
||||
name: 'Verify that the sampler works',
|
||||
check: async controls => {
|
||||
// Make sure that everything in Coordinator's /status is good
|
||||
let testSampledData: any;
|
||||
try {
|
||||
testSampledData = await postToSampler(
|
||||
{
|
||||
type: 'index',
|
||||
spec: {
|
||||
type: 'index',
|
||||
ioConfig: { type: 'index', firehose: { type: 'inline', data: '{"test":"Data"}' } },
|
||||
dataSchema: {
|
||||
dataSource: 'sample',
|
||||
parser: {
|
||||
type: 'string',
|
||||
parseSpec: {
|
||||
format: 'json',
|
||||
timestampSpec: {
|
||||
column: '!!!_no_such_column_!!!',
|
||||
missingValue: '2010-01-01T00:00:00Z',
|
||||
},
|
||||
dimensionsSpec: { dimensions: ['test'] },
|
||||
},
|
||||
},
|
||||
transformSpec: {},
|
||||
metricsSpec: [],
|
||||
granularitySpec: { queryGranularity: 'NONE' },
|
||||
},
|
||||
},
|
||||
samplerConfig: {
|
||||
numRows: 50,
|
||||
timeoutMs: 1000,
|
||||
},
|
||||
},
|
||||
'doctor',
|
||||
);
|
||||
} catch {
|
||||
controls.addIssue(`Could not use the sampler.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (deepGet(testSampledData, 'data.0.parsed.test') !== 'Data') {
|
||||
controls.addIssue(`Sampler returned incorrect data.`);
|
||||
}
|
||||
},
|
||||
},
|
||||
|
||||
// -------------------------------------
|
||||
// Check SQL
|
||||
// -------------------------------------
|
||||
{
|
||||
name: 'Verify that SQL works',
|
||||
check: async controls => {
|
||||
// Make sure that we can run the simplest query
|
||||
let sqlResult: any[];
|
||||
try {
|
||||
sqlResult = await queryDruidSql({ query: `SELECT 1 + 1 AS "two"` });
|
||||
} catch (e) {
|
||||
controls.addIssue(
|
||||
`Could not query SQL ensure that "druid.sql.enable" is set to "true" and that there is a Broker node running. Got: ${e.message}`,
|
||||
);
|
||||
controls.terminateChecks();
|
||||
return;
|
||||
}
|
||||
|
||||
if (sqlResult.length !== 1 || sqlResult[0]['two'] !== 2) {
|
||||
controls.addIssue(`Got incorrect results from a basic SQL query.`);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Verify that there are historical nodes',
|
||||
check: async controls => {
|
||||
// Make sure that there are broker and historical nodes reported from sys.servers
|
||||
let sqlResult: any[];
|
||||
try {
|
||||
sqlResult = await queryDruidSql({
|
||||
query: `SELECT
|
||||
COUNT(*) AS "historicals"
|
||||
FROM sys.servers
|
||||
WHERE "server_type" = 'historical'`,
|
||||
});
|
||||
} catch (e) {
|
||||
controls.addIssue(`Could not run a sys.servers query. Got: ${e.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (sqlResult.length === 1 && sqlResult[0]['historicals'] === 0) {
|
||||
controls.addIssue(`There do not appear to be any historical nodes.`);
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Verify that the historicals are not overfilled',
|
||||
check: async controls => {
|
||||
// Make sure that no nodes are reported that are over 95% capacity
|
||||
let sqlResult: any[];
|
||||
try {
|
||||
sqlResult = await queryDruidSql({
|
||||
query: `SELECT
|
||||
"server",
|
||||
"curr_size" * 1.0 / "max_size" AS "fill"
|
||||
FROM sys.servers
|
||||
WHERE "server_type" = 'historical' AND "curr_size" * 1.0 / "max_size" > 0.9
|
||||
ORDER BY "server" DESC`,
|
||||
});
|
||||
} catch (e) {
|
||||
controls.addIssue(`Could not run a sys.servers query. Got: ${e.message}`);
|
||||
return;
|
||||
}
|
||||
|
||||
function formatPercent(server: any): string {
|
||||
return (server['fill'] * 100).toFixed(2);
|
||||
}
|
||||
|
||||
for (const server of sqlResult) {
|
||||
if (server['fill'] > 0.95) {
|
||||
controls.addIssue(
|
||||
`Server "${server['server']}" appears to be over 95% full (is ${formatPercent(
|
||||
server,
|
||||
)}%). Increase capacity.`,
|
||||
);
|
||||
} else {
|
||||
controls.addSuggestion(
|
||||
`Server "${server['server']}" appears to be over 90% full (is ${formatPercent(
|
||||
server,
|
||||
)}%)`,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
{
|
||||
name: 'Look for time chunks that could benefit from compaction',
|
||||
check: async controls => {
|
||||
// Check for any time chunks where there is more than 1 segment and avg segment size is less than 100MB
|
||||
const dayAgo = new Date(Date.now() - 24 * 60 * 60 * 1000).toISOString();
|
||||
let sqlResult: any[];
|
||||
try {
|
||||
sqlResult = await queryDruidSql({
|
||||
query: `SELECT
|
||||
"datasource",
|
||||
COUNT(*) AS "num_bad_time_chunks"
|
||||
FROM (
|
||||
SELECT
|
||||
"datasource", "start", "end",
|
||||
AVG("size") AS "avg_segment_size_in_time_chunk",
|
||||
SUM("size") AS "total_size",
|
||||
COUNT(*) AS "num_segments"
|
||||
FROM sys.segments
|
||||
WHERE is_published = 1 AND "start" < '${dayAgo}'
|
||||
GROUP BY 1, 2, 3
|
||||
HAVING "num_segments" > 1 AND "total_size" > 1 AND "avg_segment_size_in_time_chunk" < 100000000
|
||||
)
|
||||
GROUP BY 1
|
||||
ORDER BY "num_bad_time_chunks"`,
|
||||
});
|
||||
} catch (e) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (sqlResult.length) {
|
||||
// Grab the auto-compaction definitions and ignore dataSources that already have auto-compaction
|
||||
let compactionResult: any;
|
||||
try {
|
||||
compactionResult = (await axios.get('/druid/coordinator/v1/config/compaction')).data;
|
||||
} catch (e) {
|
||||
controls.addIssue(`Could not get compaction config. Something is wrong.`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (!compactionResult.compactionConfigs) return;
|
||||
|
||||
if (!Array.isArray(compactionResult.compactionConfigs)) {
|
||||
controls.addIssue(`Got invalid value from compaction config. Something is wrong.`);
|
||||
return;
|
||||
}
|
||||
|
||||
const dataSourcesWithCompaction = compactionResult.compactionConfigs.map(
|
||||
(d: any) => d.dataSource,
|
||||
);
|
||||
|
||||
sqlResult = sqlResult.filter(d => !dataSourcesWithCompaction.includes(d['datasource']));
|
||||
|
||||
for (const datasource of sqlResult) {
|
||||
controls.addSuggestion(
|
||||
`Datasource "${
|
||||
datasource['datasource']
|
||||
}" could benefit from auto-compaction as it has ${pluralIfNeeded(
|
||||
datasource['num_bad_time_chunks'],
|
||||
'time chunk',
|
||||
)} that have multiple small segments that could be compacted.`,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
];
|
|
@ -0,0 +1,33 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
.doctor-dialog {
|
||||
&.bp3-dialog {
|
||||
margin-top: 5vh;
|
||||
top: 5%;
|
||||
}
|
||||
|
||||
.bp3-dialog-body {
|
||||
height: 70vh;
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.diagnosis {
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { render } from '@testing-library/react';
|
||||
import React from 'react';
|
||||
|
||||
import { DoctorDialog } from './doctor-dialog';
|
||||
|
||||
describe('doctor dialog', () => {
|
||||
it('matches snapshot', () => {
|
||||
const doctorDialog = <DoctorDialog onClose={() => {}} />;
|
||||
|
||||
render(doctorDialog);
|
||||
expect(document.body.lastChild).toMatchSnapshot();
|
||||
});
|
||||
});
|
|
@ -0,0 +1,200 @@
|
|||
/*
|
||||
* Licensed to the Apache Software Foundation (ASF) under one
|
||||
* or more contributor license agreements. See the NOTICE file
|
||||
* distributed with this work for additional information
|
||||
* regarding copyright ownership. The ASF licenses this file
|
||||
* to you under the Apache License, Version 2.0 (the
|
||||
* "License"); you may not use this file except in compliance
|
||||
* with the License. You may obtain a copy of the License at
|
||||
*
|
||||
* http://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
import { Button, Callout, Classes, Dialog, Intent } from '@blueprintjs/core';
|
||||
import { IconNames } from '@blueprintjs/icons';
|
||||
import React from 'react';
|
||||
|
||||
import { delay, pluralIfNeeded } from '../../utils';
|
||||
|
||||
import { DOCTOR_CHECKS } from './doctor-checks';
|
||||
|
||||
import './doctor-dialog.scss';
|
||||
|
||||
interface Diagnosis {
|
||||
type: 'suggestion' | 'issue';
|
||||
check: string;
|
||||
message: string;
|
||||
}
|
||||
|
||||
export interface DoctorDialogProps {
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
export interface DoctorDialogState {
|
||||
currentCheckIndex?: number;
|
||||
diagnoses?: Diagnosis[];
|
||||
earlyTermination?: string;
|
||||
}
|
||||
|
||||
export class DoctorDialog extends React.PureComponent<DoctorDialogProps, DoctorDialogState> {
|
||||
private mounted = false;
|
||||
|
||||
constructor(props: DoctorDialogProps, context: any) {
|
||||
super(props, context);
|
||||
this.state = {};
|
||||
}
|
||||
|
||||
componentDidMount(): void {
|
||||
this.mounted = true;
|
||||
}
|
||||
|
||||
componentWillUnmount(): void {
|
||||
this.mounted = false;
|
||||
}
|
||||
|
||||
async doChecks() {
|
||||
this.setState({ currentCheckIndex: 0, diagnoses: [] });
|
||||
|
||||
const addToDiagnoses = (diagnosis: Diagnosis) => {
|
||||
if (!this.mounted) return;
|
||||
this.setState(oldState => ({
|
||||
diagnoses: (oldState.diagnoses || []).concat(diagnosis),
|
||||
}));
|
||||
};
|
||||
|
||||
for (let i = 0; i < DOCTOR_CHECKS.length; i++) {
|
||||
if (!this.mounted) return;
|
||||
this.setState({ currentCheckIndex: i });
|
||||
const check = DOCTOR_CHECKS[i];
|
||||
let terminateChecks = false;
|
||||
|
||||
try {
|
||||
await Promise.all([
|
||||
await delay(450), // Make sure that a test takes at least this long so that the user can read the test name in the GUI,
|
||||
check.check({
|
||||
addSuggestion: (message: string) => {
|
||||
addToDiagnoses({
|
||||
type: 'suggestion',
|
||||
check: check.name,
|
||||
message,
|
||||
});
|
||||
},
|
||||
addIssue: (message: string) => {
|
||||
addToDiagnoses({
|
||||
type: 'issue',
|
||||
check: check.name,
|
||||
message,
|
||||
});
|
||||
},
|
||||
terminateChecks: () => {
|
||||
if (!this.mounted) return;
|
||||
this.setState({
|
||||
earlyTermination: `The check "${check.name}" early terminated the check suite as it has encountered a condition that would make the rest of the tests meaningless.`,
|
||||
});
|
||||
terminateChecks = true;
|
||||
},
|
||||
}),
|
||||
]);
|
||||
} catch (e) {
|
||||
addToDiagnoses({
|
||||
type: 'issue',
|
||||
check: check.name,
|
||||
message: `The check "${check.name}" encountered an unhandled exception. Please report this issue. Message: ${e.message}`,
|
||||
});
|
||||
}
|
||||
|
||||
if (terminateChecks) break;
|
||||
}
|
||||
|
||||
if (!this.mounted) return;
|
||||
this.setState({ currentCheckIndex: undefined });
|
||||
}
|
||||
|
||||
renderContent() {
|
||||
const { diagnoses, currentCheckIndex, earlyTermination } = this.state;
|
||||
|
||||
if (diagnoses) {
|
||||
let note: string;
|
||||
if (typeof currentCheckIndex === 'number') {
|
||||
note = `Running check ${currentCheckIndex + 1}/${DOCTOR_CHECKS.length}: ${
|
||||
DOCTOR_CHECKS[currentCheckIndex].name
|
||||
}`;
|
||||
} else if (earlyTermination) {
|
||||
note = `Checks stopped abruptly`;
|
||||
} else {
|
||||
note = `All ${pluralIfNeeded(DOCTOR_CHECKS.length, 'check')} completed`;
|
||||
}
|
||||
|
||||
return (
|
||||
<>
|
||||
<Callout className="diagnosis">{note}</Callout>
|
||||
{diagnoses.map((diagnosis, i) => {
|
||||
return (
|
||||
<Callout
|
||||
key={i}
|
||||
className="diagnosis"
|
||||
icon={diagnosis.type === 'suggestion' ? IconNames.FLAG : IconNames.WARNING_SIGN}
|
||||
intent={diagnosis.type === 'suggestion' ? Intent.NONE : Intent.WARNING}
|
||||
>
|
||||
{diagnosis.message}
|
||||
</Callout>
|
||||
);
|
||||
})}
|
||||
{earlyTermination && (
|
||||
<Callout className="diagnosis" intent={Intent.DANGER}>
|
||||
{earlyTermination}
|
||||
</Callout>
|
||||
)}
|
||||
{!earlyTermination && currentCheckIndex == null && diagnoses.length === 0 && (
|
||||
<Callout className="diagnosis" intent={Intent.SUCCESS}>
|
||||
No issues detected
|
||||
</Callout>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
} else {
|
||||
return (
|
||||
<div className="analyze-bar">
|
||||
<Callout className="diagnosis">
|
||||
Automated checks to troubleshoot issues with the cluster.
|
||||
</Callout>
|
||||
<Button
|
||||
text="Analyze Druid cluster"
|
||||
intent={Intent.PRIMARY}
|
||||
fill
|
||||
onClick={() => this.doChecks()}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
render(): JSX.Element {
|
||||
const { onClose } = this.props;
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
className="doctor-dialog"
|
||||
icon={IconNames.PULSE}
|
||||
onClose={onClose}
|
||||
title="Druid Doctor"
|
||||
isOpen
|
||||
canEscapeKeyClose={false}
|
||||
canOutsideClickClose={false}
|
||||
>
|
||||
<div className={Classes.DIALOG_BODY}>{this.renderContent()}</div>
|
||||
<div className={Classes.DIALOG_FOOTER}>
|
||||
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
|
||||
<Button onClick={onClose}>Close</Button>
|
||||
</div>
|
||||
</div>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
}
|
|
@ -338,3 +338,9 @@ export function copyAndAlert(copyString: string, alertMessage: string): void {
|
|||
intent: Intent.SUCCESS,
|
||||
});
|
||||
}
|
||||
|
||||
export function delay(ms: number) {
|
||||
return new Promise(resolve => {
|
||||
setTimeout(resolve, ms);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -547,7 +547,7 @@ export function getFlattenFieldFormFields() {
|
|||
}
|
||||
|
||||
export interface TransformSpec {
|
||||
transforms: Transform[];
|
||||
transforms?: Transform[];
|
||||
filter?: any;
|
||||
}
|
||||
|
||||
|
|
|
@ -151,7 +151,10 @@ export async function getOverlordModules(): Promise<string[]> {
|
|||
return statusResp.data.modules.map((m: any) => m.artifact);
|
||||
}
|
||||
|
||||
async function postToSampler(sampleSpec: SampleSpec, forStr: string): Promise<SampleResponse> {
|
||||
export async function postToSampler(
|
||||
sampleSpec: SampleSpec,
|
||||
forStr: string,
|
||||
): Promise<SampleResponse> {
|
||||
let sampleResp: any;
|
||||
try {
|
||||
sampleResp = await axios.post(`${SAMPLER_URL}?for=${forStr}`, sampleSpec);
|
||||
|
|
Loading…
Reference in New Issue