Web console: Improve data loader styling, enforce stricter TS types (#8001)

* add assets to auth exclude path

* add frame to tile page

* better empty filter state

* strict TS

* fix segments go to sql

* add unavailable segments

* factor out sugestable input

* fix tests

* update datasources sql

* no depricated extend

* add index spec to tuning configs

* fix scss lint
This commit is contained in:
Vadim Ogievetsky 2019-06-30 19:33:16 -07:00 committed by Fangjin Yang
parent 2831944056
commit f16f13cf61
114 changed files with 1023 additions and 1109 deletions

View File

@ -65,6 +65,7 @@ public class RouterJettyServerInitializer implements JettyServerInitializer
protected static final List<String> UNSECURED_PATHS_FOR_UI = ImmutableList.of( protected static final List<String> UNSECURED_PATHS_FOR_UI = ImmutableList.of(
"/", "/",
"/coordinator-console/*", "/coordinator-console/*",
"/assets/*",
"/public/*", "/public/*",
"/old-console/*", "/old-console/*",
"/pages/*", "/pages/*",

View File

@ -1,6 +1,6 @@
{ {
"name": "web-console", "name": "web-console",
"version": "0.15.0", "version": "0.16.0",
"description": "A web console for Apache Druid", "description": "A web console for Apache Druid",
"author": "Imply Data Inc.", "author": "Imply Data Inc.",
"license": "Apache-2.0", "license": "Apache-2.0",
@ -37,7 +37,7 @@
"compile": "./script/build", "compile": "./script/build",
"pretest": "./script/build", "pretest": "./script/build",
"run": "./script/run", "run": "./script/run",
"test": "npm run tslint && jest --silent 2>&1", "test": "npm run tslint && npm run stylelint && jest --silent 2>&1",
"coverage": "jest --coverage", "coverage": "jest --coverage",
"update-snapshots": "jest -u", "update-snapshots": "jest -u",
"tslint": "./node_modules/.bin/tslint -c tslint.json --project tsconfig.json --formatters-dir ./node_modules/awesome-code-style/formatter 'src/**/*.ts?(x)'", "tslint": "./node_modules/.bin/tslint -c tslint.json --project tsconfig.json --formatters-dir ./node_modules/awesome-code-style/formatter 'src/**/*.ts?(x)'",

View File

@ -85,7 +85,7 @@ import React from 'react';
import './${name}.scss'; import './${name}.scss';
export interface ${camelName}Props extends React.Props<any> { export interface ${camelName}Props {
} }
export interface ${camelName}State { export interface ${camelName}State {
@ -100,7 +100,7 @@ export class ${camelName} extends React.PureComponent<${camelName}Props, ${camel
render() { render() {
return <div className="${name}"> return <div className="${name}">
Stuff...
</div>; </div>;
} }
} }

View File

@ -21,7 +21,7 @@ import React from 'react';
import './react-table-custom-pagination.scss'; import './react-table-custom-pagination.scss';
interface ReactTableCustomPaginationProps extends React.Props<any> { interface ReactTableCustomPaginationProps {
pages: number; pages: number;
page: number; page: number;
showPageSizeOptions: boolean; showPageSizeOptions: boolean;

View File

@ -38,7 +38,7 @@ class NoData extends React.PureComponent {
Object.assign(ReactTableDefaults, { Object.assign(ReactTableDefaults, {
className: '-striped -highlight', className: '-striped -highlight',
defaultFilterMethod: (filter: Filter, row: any, column: any) => { defaultFilterMethod: (filter: Filter, row: any) => {
const id = filter.pivotId || filter.id; const id = filter.pivotId || filter.id;
return booleanCustomTableFilter(filter, row[id]); return booleanCustomTableFilter(filter, row[id]);
}, },

View File

@ -25,7 +25,7 @@ import { ActionIcon } from '../action-icon/action-icon';
import './action-cell.scss'; import './action-cell.scss';
export interface ActionCellProps extends React.Props<any> { export interface ActionCellProps {
onDetail?: () => void; onDetail?: () => void;
actions?: BasicAction[]; actions?: BasicAction[];
} }

View File

@ -22,7 +22,7 @@ import React from 'react';
import './action-icon.scss'; import './action-icon.scss';
export interface ActionIconProps extends React.Props<any> { export interface ActionIconProps {
className?: string; className?: string;
icon: IconName; icon: IconName;
onClick?: () => void; onClick?: () => void;

View File

@ -190,7 +190,7 @@ exports[`auto-form snapshot matches snapshot 1`] = `
class="bp3-form-content" class="bp3-form-content"
> >
<div <div
class="bp3-input-group" class="bp3-input-group suggestible-input"
> >
<input <input
class="bp3-input" class="bp3-input"

View File

@ -35,7 +35,7 @@ describe('auto-form snapshot', () => {
{ name: 'testSeven', type: 'json' }, { name: 'testSeven', type: 'json' },
]} ]}
model={String} model={String}
onChange={(newModel: Record<string, any>) => {}} onChange={() => {}}
/> />
); );
const { container } = render(autoForm); const { container } = render(autoForm);

View File

@ -16,32 +16,17 @@
* limitations under the License. * limitations under the License.
*/ */
import { import { FormGroup, HTMLSelect, Icon, NumericInput, Popover } from '@blueprintjs/core';
Button,
FormGroup,
HTMLSelect,
Icon,
InputGroup,
Menu,
MenuItem,
NumericInput,
Popover,
Position,
} from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons'; import { IconNames } from '@blueprintjs/icons';
import React from 'react'; import React from 'react';
import { deepDelete, deepGet, deepSet } from '../../utils/object-change'; import { deepDelete, deepGet, deepSet } from '../../utils/object-change';
import { ArrayInput } from '../array-input/array-input'; import { ArrayInput } from '../array-input/array-input';
import { JSONInput } from '../json-input/json-input'; import { JSONInput } from '../json-input/json-input';
import { SuggestibleInput, SuggestionGroup } from '../suggestible-input/suggestible-input';
import './auto-form.scss'; import './auto-form.scss';
export interface SuggestionGroup {
group: string;
suggestions: string[];
}
export interface Field<T> { export interface Field<T> {
name: string; name: string;
label?: string; label?: string;
@ -55,7 +40,7 @@ export interface Field<T> {
min?: number; min?: number;
} }
export interface AutoFormProps<T> extends React.Props<any> { export interface AutoFormProps<T> {
fields: Field<T>[]; fields: Field<T>[];
model: T | null; model: T | null;
onChange: (newModel: T) => void; onChange: (newModel: T) => void;
@ -64,13 +49,13 @@ export interface AutoFormProps<T> extends React.Props<any> {
large?: boolean; large?: boolean;
} }
export interface AutoFormState<T> { export interface AutoFormState {
jsonInputsValidity: any; jsonInputsValidity: any;
} }
export class AutoForm<T extends Record<string, any>> extends React.PureComponent< export class AutoForm<T extends Record<string, any>> extends React.PureComponent<
AutoFormProps<T>, AutoFormProps<T>,
AutoFormState<T> AutoFormState
> { > {
static makeLabelName(label: string): string { static makeLabelName(label: string): string {
let newLabel = label let newLabel = label
@ -159,42 +144,11 @@ export class AutoForm<T extends Record<string, any>> extends React.PureComponent
private renderStringInput(field: Field<T>, sanitize?: (str: string) => string): JSX.Element { private renderStringInput(field: Field<T>, sanitize?: (str: string) => string): JSX.Element {
const { model, large } = this.props; const { model, large } = this.props;
const suggestionsMenu = field.suggestions ? (
<Menu>
{field.suggestions.map(suggestion => {
if (typeof suggestion === 'string') {
return (
<MenuItem
key={suggestion}
text={suggestion}
onClick={() => this.fieldChange(field, suggestion)}
/>
);
} else {
return (
<MenuItem key={suggestion.group} text={suggestion.group}>
{suggestion.suggestions.map(suggestion => (
<MenuItem
key={suggestion}
text={suggestion}
onClick={() => this.fieldChange(field, suggestion)}
/>
))}
</MenuItem>
);
}
})}
</Menu>
) : (
undefined
);
const modalValue = deepGet(model as any, field.name); const modalValue = deepGet(model as any, field.name);
return ( return (
<InputGroup <SuggestibleInput
value={modalValue != null ? modalValue : field.defaultValue || ''} value={modalValue != null ? modalValue : field.defaultValue || ''}
onChange={(e: any) => { onValueChange={v => {
let v = e.target.value;
if (sanitize) v = sanitize(v); if (sanitize) v = sanitize(v);
this.fieldChange(field, v); this.fieldChange(field, v);
}} }}
@ -202,13 +156,7 @@ export class AutoForm<T extends Record<string, any>> extends React.PureComponent
if (modalValue === '') this.fieldChange(field, undefined); if (modalValue === '') this.fieldChange(field, undefined);
}} }}
placeholder={field.placeholder} placeholder={field.placeholder}
rightElement={ suggestions={field.suggestions}
suggestionsMenu && (
<Popover content={suggestionsMenu} position={Position.BOTTOM_RIGHT} autoFocus={false}>
<Button icon={IconNames.CARET_DOWN} minimal />
</Popover>
)
}
large={large} large={large}
disabled={field.disabled} disabled={field.disabled}
/> />

View File

@ -20,7 +20,7 @@ import React from 'react';
import './center-message.scss'; import './center-message.scss';
export interface CenterMessageProps extends React.Props<any> {} export interface CenterMessageProps {}
export class CenterMessage extends React.PureComponent<CenterMessageProps> { export class CenterMessage extends React.PureComponent<CenterMessageProps> {
render() { render() {

View File

@ -1,6 +1,6 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP // Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`decribe clearable-input matches snapshot 1`] = ` exports[`clearable-input matches snapshot 1`] = `
<div <div
class="bp3-input-group clearable-input testClassName" class="bp3-input-group clearable-input testClassName"
> >

View File

@ -21,16 +21,16 @@ import { render } from 'react-testing-library';
import { ClearableInput } from './clearable-input'; import { ClearableInput } from './clearable-input';
describe('decribe clearable-input', () => { describe('clearable-input', () => {
it('matches snapshot', () => { it('matches snapshot', () => {
const centerMessage = ( const centerMessage = (
<ClearableInput <ClearableInput
className={'testClassName'} className={'testClassName'}
value={'testValue'} value={'testValue'}
placeholder={'testPlaceholder'} placeholder={'testPlaceholder'}
onChange={(value: string) => null} onChange={() => null}
> >
;<div>Hello World</div> <div>Hello World</div>
</ClearableInput> </ClearableInput>
); );

View File

@ -21,7 +21,7 @@ import { IconNames } from '@blueprintjs/icons';
import classNames from 'classnames'; import classNames from 'classnames';
import React from 'react'; import React from 'react';
export interface ClearableInputProps extends React.Props<any> { export interface ClearableInputProps {
className?: string; className?: string;
value: string; value: string;
onChange: (value: string) => void; onChange: (value: string) => void;

View File

@ -18,7 +18,7 @@
import React from 'react'; import React from 'react';
export interface ExternalLinkProps extends React.Props<any> { export interface ExternalLinkProps {
href: string; href: string;
} }

View File

@ -22,7 +22,6 @@ import {
Button, Button,
Intent, Intent,
Menu, Menu,
MenuDivider,
MenuItem, MenuItem,
Navbar, Navbar,
NavbarDivider, NavbarDivider,
@ -34,8 +33,8 @@ import { IconNames } from '@blueprintjs/icons';
import React from 'react'; import React from 'react';
import { AboutDialog } from '../../dialogs/about-dialog/about-dialog'; import { AboutDialog } from '../../dialogs/about-dialog/about-dialog';
import { CoordinatorDynamicConfigDialog } from '../../dialogs/coordinator-dynamic-config/coordinator-dynamic-config'; import { CoordinatorDynamicConfigDialog } from '../../dialogs/coordinator-dynamic-config-dialog/coordinator-dynamic-config-dialog';
import { OverlordDynamicConfigDialog } from '../../dialogs/overlord-dynamic-config/overlord-dynamic-config'; import { OverlordDynamicConfigDialog } from '../../dialogs/overlord-dynamic-config-dialog/overlord-dynamic-config-dialog';
import { import {
DRUID_DOCS, DRUID_DOCS,
DRUID_GITHUB, DRUID_GITHUB,
@ -56,7 +55,7 @@ export type HeaderActiveTab =
| 'servers' | 'servers'
| 'lookups'; | 'lookups';
export interface HeaderBarProps extends React.Props<any> { export interface HeaderBarProps {
active: HeaderActiveTab; active: HeaderActiveTab;
hideLegacy: boolean; hideLegacy: boolean;
} }

View File

@ -17,10 +17,9 @@
*/ */
import { Button, Collapse, TextArea } from '@blueprintjs/core'; import { Button, Collapse, TextArea } from '@blueprintjs/core';
import classNames from 'classnames';
import React from 'react'; import React from 'react';
interface JSONCollapseProps extends React.Props<any> { interface JSONCollapseProps {
stringValue: string; stringValue: string;
buttonText: string; buttonText: string;
} }

View File

@ -23,7 +23,7 @@ import { JSONInput } from './json-input';
describe('json input', () => { describe('json input', () => {
it('matches snapshot', () => { it('matches snapshot', () => {
const jsonCollapse = <JSONInput onChange={(newJSONValue: any) => {}} value={'test'} />; const jsonCollapse = <JSONInput onChange={() => {}} value={'test'} />;
const { container } = render(jsonCollapse); const { container } = render(jsonCollapse);
expect(container.firstChild).toMatchSnapshot(); expect(container.firstChild).toMatchSnapshot();
}); });

View File

@ -21,7 +21,7 @@ import AceEditor from 'react-ace';
import { parseStringToJSON, stringifyJSON, validJson } from '../../utils'; import { parseStringToJSON, stringifyJSON, validJson } from '../../utils';
interface JSONInputProps extends React.Props<any> { interface JSONInputProps {
onChange: (newJSONValue: any) => void; onChange: (newJSONValue: any) => void;
value: any; value: any;
updateInputValidity?: (valueValid: boolean) => void; updateInputValidity?: (valueValid: boolean) => void;

View File

@ -20,7 +20,7 @@ import React from 'react';
import './loader.scss'; import './loader.scss';
export interface LoaderProps extends React.Props<any> { export interface LoaderProps {
loadingText?: string; loadingText?: string;
loading?: boolean; // This is needed so that this component can be used as a LoadingComponent in react table loading?: boolean; // This is needed so that this component can be used as a LoadingComponent in react table
} }

View File

@ -21,7 +21,7 @@ import React from 'react';
import { LocalStorageKeys } from '../../utils'; import { LocalStorageKeys } from '../../utils';
import { TimedButton } from '../timed-button/timed-button'; import { TimedButton } from '../timed-button/timed-button';
export interface RefreshButtonProps extends React.Props<any> { export interface RefreshButtonProps {
onRefresh: (auto: boolean) => void; onRefresh: (auto: boolean) => void;
localStorageKey?: LocalStorageKeys; localStorageKey?: LocalStorageKeys;
} }
@ -45,10 +45,10 @@ export class RefreshButton extends React.PureComponent<RefreshButtonProps> {
return ( return (
<TimedButton <TimedButton
defaultValue={30000} defaultValue={30000}
label={'Auto refresh every:'} label="Auto refresh every:"
intervals={intervals} intervals={intervals}
icon={IconNames.REFRESH} icon={IconNames.REFRESH}
text={'Refresh'} text="Refresh"
onRefresh={onRefresh} onRefresh={onRefresh}
localStorageKey={localStorageKey} localStorageKey={localStorageKey}
/> />

View File

@ -19,7 +19,7 @@
import React from 'react'; import React from 'react';
import { render } from 'react-testing-library'; import { render } from 'react-testing-library';
import { Rule, RuleEditor } from './rule-editor'; import { RuleEditor } from './rule-editor';
describe('rule editor', () => { describe('rule editor', () => {
it('matches snapshot', () => { it('matches snapshot', () => {
@ -27,7 +27,7 @@ describe('rule editor', () => {
<RuleEditor <RuleEditor
rule={{ type: 'loadForever' }} rule={{ type: 'loadForever' }}
tiers={['test', 'test', 'test']} tiers={['test', 'test', 'test']}
onChange={(newRule: Rule) => null} onChange={() => null}
onDelete={() => null} onDelete={() => null}
moveUp={null} moveUp={null}
moveDown={null} moveDown={null}

View File

@ -28,7 +28,6 @@ import {
TagInput, TagInput,
} from '@blueprintjs/core'; } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons'; import { IconNames } from '@blueprintjs/icons';
import axios from 'axios';
import React from 'react'; import React from 'react';
import './rule-editor.scss'; import './rule-editor.scss';
@ -53,7 +52,7 @@ export interface Rule {
export type LoadType = 'load' | 'drop' | 'broadcast'; export type LoadType = 'load' | 'drop' | 'broadcast';
export type TimeType = 'Forever' | 'ByInterval' | 'ByPeriod'; export type TimeType = 'Forever' | 'ByInterval' | 'ByPeriod';
export interface RuleEditorProps extends React.Props<any> { export interface RuleEditorProps {
rule: Rule; rule: Rule;
tiers: any[]; tiers: any[];
onChange: (newRule: Rule) => void; onChange: (newRule: Rule) => void;
@ -241,7 +240,7 @@ export class RuleEditor extends React.PureComponent<RuleEditorProps, RuleEditorS
} }
render() { render() {
const { tiers, onChange, rule, onDelete, moveUp, moveDown } = this.props; const { onChange, rule, onDelete, moveUp, moveDown } = this.props;
const { isOpen } = this.state; const { isOpen } = this.state;
if (!rule) return null; if (!rule) return null;

View File

@ -16,7 +16,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { Button, ButtonGroup, InputGroup, Intent, TextArea } from '@blueprintjs/core'; import { Button, ButtonGroup, Intent, TextArea } from '@blueprintjs/core';
import axios from 'axios'; import axios from 'axios';
import copy from 'copy-to-clipboard'; import copy from 'copy-to-clipboard';
import React from 'react'; import React from 'react';
@ -27,7 +27,7 @@ import { downloadFile } from '../../utils';
import './show-json.scss'; import './show-json.scss';
export interface ShowJsonProps extends React.Props<any> { export interface ShowJsonProps {
endpoint: string; endpoint: string;
transform?: (x: any) => any; transform?: (x: any) => any;
downloadFilename?: string; downloadFilename?: string;

View File

@ -16,7 +16,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { Button, ButtonGroup, Checkbox, InputGroup, Intent, TextArea } from '@blueprintjs/core'; import { Button, ButtonGroup, Checkbox, Intent } from '@blueprintjs/core';
import axios from 'axios'; import axios from 'axios';
import copy from 'copy-to-clipboard'; import copy from 'copy-to-clipboard';
import React from 'react'; import React from 'react';
@ -35,7 +35,7 @@ function removeFirstPartialLine(log: string): string {
return lines.join('\n'); return lines.join('\n');
} }
export interface ShowLogProps extends React.Props<any> { export interface ShowLogProps {
endpoint: string; endpoint: string;
downloadFilename?: string; downloadFilename?: string;
tailOffset?: number; tailOffset?: number;

View File

@ -0,0 +1,51 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`suggestible input matches snapshot 1`] = `
<div
class="bp3-input-group suggestible-input"
>
<input
class="bp3-input"
style="padding-right: 0px;"
suggestions="a,b,c"
type="text"
value=""
/>
<span
class="bp3-input-action"
>
<span
class="bp3-popover-wrapper"
>
<span
class="bp3-popover-target"
>
<button
class="bp3-button bp3-minimal"
type="button"
>
<span
class="bp3-icon bp3-icon-caret-down"
icon="caret-down"
>
<svg
data-icon="caret-down"
height="16"
viewBox="0 0 16 16"
width="16"
>
<desc>
caret-down
</desc>
<path
d="M12 6.5c0-.28-.22-.5-.5-.5h-7a.495.495 0 0 0-.37.83l3.5 4c.09.1.22.17.37.17s.28-.07.37-.17l3.5-4c.08-.09.13-.2.13-.33z"
fill-rule="evenodd"
/>
</svg>
</span>
</button>
</span>
</span>
</span>
</div>
`;

View File

@ -19,25 +19,15 @@
import React from 'react'; import React from 'react';
import { render } from 'react-testing-library'; import { render } from 'react-testing-library';
import { LookupEditDialog } from '../lookup-edit-dialog/lookup-edit-dialog'; import { SuggestibleInput } from './suggestible-input';
describe('overload dynamic config', () => { describe('suggestible input', () => {
it('matches snapshot', () => { it('matches snapshot', () => {
const lookupEditDialog = ( const suggestibleInput = (
<LookupEditDialog <SuggestibleInput onValueChange={() => {}} suggestions={['a', 'b', 'c']} />
isOpen
onClose={() => null}
onSubmit={() => null}
onChange={() => null}
lookupName={'test'}
lookupTier={'test'}
lookupVersion={'test'}
lookupSpec={'test'}
isEdit={false}
allLookupTiers={['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j']}
/>
); );
const { container } = render(lookupEditDialog, { container: document.body });
const { container } = render(suggestibleInput);
expect(container.firstChild).toMatchSnapshot(); expect(container.firstChild).toMatchSnapshot();
}); });
}); });

View File

@ -0,0 +1,106 @@
/*
* 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,
HTMLInputProps,
InputGroup,
Menu,
MenuItem,
Popover,
Position,
} from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import classNames from 'classnames';
import React from 'react';
export interface SuggestionGroup {
group: string;
suggestions: string[];
}
export interface SuggestibleInputProps extends HTMLInputProps {
onValueChange: (newValue: string) => void;
suggestions?: (string | SuggestionGroup)[];
large?: boolean;
}
export class SuggestibleInput extends React.PureComponent<SuggestibleInputProps> {
constructor(props: SuggestibleInputProps, context: any) {
super(props, context);
// this.state = {};
}
renderSuggestionsMenu() {
const { suggestions, onValueChange } = this.props;
if (!suggestions) return undefined;
return (
<Menu>
{suggestions.map(suggestion => {
if (typeof suggestion === 'string') {
return (
<MenuItem
key={suggestion}
text={suggestion}
onClick={() => onValueChange(suggestion)}
/>
);
} else {
return (
<MenuItem key={suggestion.group} text={suggestion.group}>
{suggestion.suggestions.map(suggestion => (
<MenuItem
key={suggestion}
text={suggestion}
onClick={() => onValueChange(suggestion)}
/>
))}
</MenuItem>
);
}
})}
</Menu>
);
}
render() {
const { className, value, defaultValue, onValueChange, large, ...rest } = this.props;
const suggestionsMenu = this.renderSuggestionsMenu();
return (
<InputGroup
className={classNames('suggestible-input', className)}
value={value as string}
defaultValue={defaultValue as string}
onChange={(e: any) => {
onValueChange(e.target.value);
}}
rightElement={
suggestionsMenu && (
<Popover content={suggestionsMenu} position={Position.BOTTOM_RIGHT} autoFocus={false}>
<Button icon={IconNames.CARET_DOWN} minimal />
</Popover>
)
}
large={large}
{...rest}
/>
);
}
}

View File

@ -16,17 +16,14 @@
* limitations under the License. * limitations under the License.
*/ */
import { Intent } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons'; import { IconNames } from '@blueprintjs/icons';
import copy from 'copy-to-clipboard';
import React from 'react'; import React from 'react';
import { AppToaster } from '../../singletons/toaster';
import { ActionIcon } from '../action-icon/action-icon'; import { ActionIcon } from '../action-icon/action-icon';
import './table-cell.scss'; import './table-cell.scss';
export interface NullTableCellProps extends React.Props<any> { export interface NullTableCellProps {
value?: any; value?: any;
timestamp?: boolean; timestamp?: boolean;
unparseable?: boolean; unparseable?: boolean;

View File

@ -26,7 +26,7 @@ describe('table column', () => {
const tableColumn = ( const tableColumn = (
<TableColumnSelector <TableColumnSelector
columns={['a', 'b', 'c']} columns={['a', 'b', 'c']}
onChange={(column: string) => {}} onChange={() => {}}
tableColumnsHidden={['a', 'b', 'c']} tableColumnsHidden={['a', 'b', 'c']}
/> />
); );

View File

@ -16,7 +16,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { Button, Checkbox, FormGroup, Menu, Popover, Position } from '@blueprintjs/core'; import { Button, Menu, Popover, Position } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons'; import { IconNames } from '@blueprintjs/icons';
import React from 'react'; import React from 'react';
@ -24,7 +24,7 @@ import { MenuCheckbox } from '../menu-checkbox/menu-checkbox';
import './table-column-selector.scss'; import './table-column-selector.scss';
interface TableColumnSelectorProps extends React.Props<any> { interface TableColumnSelectorProps {
columns: string[]; columns: string[];
onChange: (column: string) => void; onChange: (column: string) => void;
tableColumnsHidden: string[]; tableColumnsHidden: string[];

View File

@ -17,6 +17,5 @@
*/ */
.timed-button { .timed-button {
padding: 10px 10px 5px 10px; padding: 10px 10px 5px 10px;
} }

View File

@ -41,7 +41,9 @@ import {
import './console-application.scss'; import './console-application.scss';
export interface ConsoleApplicationProps extends React.Props<any> { type Capabilities = 'working-with-sql' | 'working-without-sql' | 'broken';
export interface ConsoleApplicationProps {
hideLegacy: boolean; hideLegacy: boolean;
baseURL?: string; baseURL?: string;
customHeaderName?: string; customHeaderName?: string;
@ -60,11 +62,9 @@ export class ConsoleApplication extends React.PureComponent<
> { > {
static MESSAGE_KEY = 'druid-console-message'; static MESSAGE_KEY = 'druid-console-message';
static MESSAGE_DISMISSED = 'dismissed'; static MESSAGE_DISMISSED = 'dismissed';
private capabilitiesQueryManager: QueryManager<string, string>; private capabilitiesQueryManager: QueryManager<string, Capabilities>;
static async discoverCapabilities(): Promise< static async discoverCapabilities(): Promise<Capabilities> {
'working-with-sql' | 'working-without-sql' | 'broken'
> {
try { try {
await axios.post('/druid/v2/sql', { query: 'SELECT 1337' }); await axios.post('/druid/v2/sql', { query: 'SELECT 1337' });
} catch (e) { } catch (e) {
@ -109,13 +109,13 @@ export class ConsoleApplication extends React.PureComponent<
}); });
} }
private supervisorId: string | null; private supervisorId: string | undefined;
private taskId: string | null; private taskId: string | undefined;
private openDialog: string | null; private openDialog: string | undefined;
private datasource: string | null; private datasource: string | undefined;
private onlyUnavailable: boolean | null; private onlyUnavailable: boolean | undefined;
private initQuery: string | null; private initQuery: string | undefined;
private middleManager: string | null; private middleManager: string | undefined;
constructor(props: ConsoleApplicationProps, context: any) { constructor(props: ConsoleApplicationProps, context: any) {
super(props, context); super(props, context);
@ -134,16 +134,16 @@ export class ConsoleApplication extends React.PureComponent<
} }
this.capabilitiesQueryManager = new QueryManager({ this.capabilitiesQueryManager = new QueryManager({
processQuery: async (query: string) => { processQuery: async () => {
const capabilities = await ConsoleApplication.discoverCapabilities(); const capabilities = await ConsoleApplication.discoverCapabilities();
if (capabilities !== 'working-with-sql') { if (capabilities !== 'working-with-sql') {
ConsoleApplication.shownNotifications(capabilities); ConsoleApplication.shownNotifications(capabilities);
} }
return capabilities; return capabilities;
}, },
onStateChange: ({ result, loading, error }) => { onStateChange: ({ result, loading }) => {
this.setState({ this.setState({
noSqlMode: result === 'working-with-sql' ? false : true, noSqlMode: result !== 'working-with-sql',
capabilitiesLoading: loading, capabilitiesLoading: loading,
}); });
}, },
@ -160,13 +160,13 @@ export class ConsoleApplication extends React.PureComponent<
private resetInitialsWithDelay() { private resetInitialsWithDelay() {
setTimeout(() => { setTimeout(() => {
this.taskId = null; this.taskId = undefined;
this.supervisorId = null; this.supervisorId = undefined;
this.openDialog = null; this.openDialog = undefined;
this.datasource = null; this.datasource = undefined;
this.onlyUnavailable = null; this.onlyUnavailable = undefined;
this.initQuery = null; this.initQuery = undefined;
this.middleManager = null; this.middleManager = undefined;
}, 50); }, 50);
} }
@ -177,7 +177,7 @@ export class ConsoleApplication extends React.PureComponent<
this.resetInitialsWithDelay(); this.resetInitialsWithDelay();
}; };
private goToTask = (taskId: string | null, openDialog?: string) => { private goToTask = (taskId: string | undefined, openDialog?: string) => {
this.taskId = taskId; this.taskId = taskId;
if (openDialog) this.openDialog = openDialog; if (openDialog) this.openDialog = openDialog;
window.location.hash = 'tasks'; window.location.hash = 'tasks';

View File

@ -24,7 +24,7 @@ import { AboutDialog } from './about-dialog';
describe('about dialog', () => { describe('about dialog', () => {
it('matches snapshot', () => { it('matches snapshot', () => {
const aboutDialog = <AboutDialog onClose={() => null} />; const aboutDialog = <AboutDialog onClose={() => null} />;
const { container } = render(aboutDialog, { container: document.body }); render(aboutDialog);
expect(container.firstChild).toMatchSnapshot(); expect(document.body.lastChild).toMatchSnapshot();
}); });
}); });

View File

@ -28,7 +28,7 @@ import {
DRUID_WEBSITE, DRUID_WEBSITE,
} from '../../variables'; } from '../../variables';
export interface AboutDialogProps extends React.Props<any> { export interface AboutDialogProps {
onClose: () => void; onClose: () => void;
} }

View File

@ -28,13 +28,13 @@ describe('async action dialog', () => {
action={() => { action={() => {
return Promise.resolve(); return Promise.resolve();
}} }}
onClose={(success: boolean) => null} onClose={() => null}
confirmButtonText={'test'} confirmButtonText={'test'}
successText={'test'} successText={'test'}
failText={'test'} failText={'test'}
/> />
); );
const { container } = render(asyncActionDialog, { container: document.body }); render(asyncActionDialog);
expect(container.firstChild).toMatchSnapshot(); expect(document.body.lastChild).toMatchSnapshot();
}); });
}); });

View File

@ -16,25 +16,14 @@
* limitations under the License. * limitations under the License.
*/ */
import { import { Button, Classes, Dialog, Icon, Intent, ProgressBar } from '@blueprintjs/core';
Button,
ButtonGroup,
Classes,
Dialog,
FormGroup,
Icon,
Intent,
NumericInput,
ProgressBar,
TagInput,
} from '@blueprintjs/core';
import { IconName } from '@blueprintjs/icons'; import { IconName } from '@blueprintjs/icons';
import classNames from 'classnames'; import classNames from 'classnames';
import React from 'react'; import React from 'react';
import { AppToaster } from '../../singletons/toaster'; import { AppToaster } from '../../singletons/toaster';
export interface AsyncAlertDialogProps extends React.Props<any> { export interface AsyncAlertDialogProps {
action: null | (() => Promise<void>); action: null | (() => Promise<void>);
onClose: (success: boolean) => void; onClose: (success: boolean) => void;
confirmButtonText: string; confirmButtonText: string;

View File

@ -240,7 +240,7 @@ exports[`compaction dialog matches snapshot 1`] = `
class="bp3-form-content" class="bp3-form-content"
> >
<div <div
class="bp3-input-group" class="bp3-input-group suggestible-input"
> >
<input <input
class="bp3-input" class="bp3-input"

View File

@ -26,13 +26,13 @@ describe('compaction dialog', () => {
const compactionDialog = ( const compactionDialog = (
<CompactionDialog <CompactionDialog
onClose={() => null} onClose={() => null}
onSave={(config: any) => null} onSave={() => null}
onDelete={() => null} onDelete={() => null}
datasource={'test'} datasource={'test'}
configData={'test'} configData={'test'}
/> />
); );
const { container } = render(compactionDialog, { container: document.body }); render(compactionDialog);
expect(container.firstChild).toMatchSnapshot(); expect(document.body.lastChild).toMatchSnapshot();
}); });
}); });

View File

@ -23,7 +23,7 @@ import { AutoForm } from '../../components';
import './compaction-dialog.scss'; import './compaction-dialog.scss';
export interface CompactionDialogProps extends React.Props<any> { export interface CompactionDialogProps {
onClose: () => void; onClose: () => void;
onSave: (config: any) => void; onSave: (config: any) => void;
onDelete: () => void; onDelete: () => void;

View File

@ -16,7 +16,7 @@ exports[`coordinator dynamic config matches snapshot 1`] = `
tabindex="0" tabindex="0"
> >
<div <div
class="bp3-dialog snitch-dialog coordinator-dynamic-config" class="bp3-dialog snitch-dialog coordinator-dynamic-config-dialog"
> >
<div <div
class="bp3-dialog-header" class="bp3-dialog-header"
@ -76,16 +76,6 @@ exports[`coordinator dynamic config matches snapshot 1`] = `
<div <div
class="bp3-dialog-footer-actions" class="bp3-dialog-footer-actions"
> >
<button
class="bp3-button bp3-minimal left-align-button"
type="button"
>
<span
class="bp3-button-text"
>
History
</span>
</button>
<button <button
class="bp3-button bp3-intent-primary" class="bp3-button bp3-intent-primary"
type="button" type="button"

View File

@ -16,7 +16,7 @@
* limitations under the License. * limitations under the License.
*/ */
.coordinator-dynamic-config { .coordinator-dynamic-config-dialog {
&.bp3-dialog { &.bp3-dialog {
margin-top: 5vh; margin-top: 5vh;
top: 5%; top: 5%;

View File

@ -19,12 +19,12 @@
import React from 'react'; import React from 'react';
import { render } from 'react-testing-library'; import { render } from 'react-testing-library';
import { CoordinatorDynamicConfigDialog } from './coordinator-dynamic-config'; import { CoordinatorDynamicConfigDialog } from './coordinator-dynamic-config-dialog';
describe('coordinator dynamic config', () => { describe('coordinator dynamic config', () => {
it('matches snapshot', () => { it('matches snapshot', () => {
const coordinatorDynamicConfig = <CoordinatorDynamicConfigDialog onClose={() => null} />; const coordinatorDynamicConfig = <CoordinatorDynamicConfigDialog onClose={() => null} />;
const { container } = render(coordinatorDynamicConfig, { container: document.body }); render(coordinatorDynamicConfig);
expect(container.firstChild).toMatchSnapshot(); expect(document.body.lastChild).toMatchSnapshot();
}); });
}); });

View File

@ -26,9 +26,9 @@ import { AppToaster } from '../../singletons/toaster';
import { getDruidErrorMessage, QueryManager } from '../../utils'; import { getDruidErrorMessage, QueryManager } from '../../utils';
import { SnitchDialog } from '../snitch-dialog/snitch-dialog'; import { SnitchDialog } from '../snitch-dialog/snitch-dialog';
import './coordinator-dynamic-config.scss'; import './coordinator-dynamic-config-dialog.scss';
export interface CoordinatorDynamicConfigDialogProps extends React.Props<any> { export interface CoordinatorDynamicConfigDialogProps {
onClose: () => void; onClose: () => void;
} }
@ -41,7 +41,7 @@ export class CoordinatorDynamicConfigDialog extends React.PureComponent<
CoordinatorDynamicConfigDialogProps, CoordinatorDynamicConfigDialogProps,
CoordinatorDynamicConfigDialogState CoordinatorDynamicConfigDialogState
> { > {
private historyQueryManager: QueryManager<string, any>; private historyQueryManager: QueryManager<null, any>;
constructor(props: CoordinatorDynamicConfigDialogProps) { constructor(props: CoordinatorDynamicConfigDialogProps) {
super(props); super(props);
@ -49,24 +49,24 @@ export class CoordinatorDynamicConfigDialog extends React.PureComponent<
dynamicConfig: null, dynamicConfig: null,
historyRecords: [], historyRecords: [],
}; };
}
componentDidMount() {
this.getClusterConfig();
this.historyQueryManager = new QueryManager({ this.historyQueryManager = new QueryManager({
processQuery: async query => { processQuery: async () => {
const historyResp = await axios(`/druid/coordinator/v1/config/history?count=100`); const historyResp = await axios(`/druid/coordinator/v1/config/history?count=100`);
return historyResp.data; return historyResp.data;
}, },
onStateChange: ({ result, loading, error }) => { onStateChange: ({ result }) => {
this.setState({ this.setState({
historyRecords: result, historyRecords: result,
}); });
}, },
}); });
}
this.historyQueryManager.runQuery(`dummy`); componentDidMount() {
this.getClusterConfig();
this.historyQueryManager.runQuery(null);
} }
async getClusterConfig() { async getClusterConfig() {
@ -118,7 +118,7 @@ export class CoordinatorDynamicConfigDialog extends React.PureComponent<
return ( return (
<SnitchDialog <SnitchDialog
className="coordinator-dynamic-config" className="coordinator-dynamic-config-dialog"
isOpen isOpen
onSave={this.saveClusterConfig} onSave={this.saveClusterConfig}
onClose={onClose} onClose={onClose}

View File

@ -32,7 +32,7 @@ describe('history dialog', () => {
isOpen isOpen
/> />
); );
const { container } = render(historyDialog, { container: document.body }); render(historyDialog);
expect(container.firstChild).toMatchSnapshot(); expect(document.body.lastChild).toMatchSnapshot();
}); });
}); });

View File

@ -24,7 +24,7 @@ import { JSONCollapse } from '../../components';
import './history-dialog.scss'; import './history-dialog.scss';
interface HistoryDialogProps extends IDialogProps { interface HistoryDialogProps extends IDialogProps {
historyRecords: any; historyRecords: any[];
} }
interface HistoryDialogState {} interface HistoryDialogState {}
@ -45,13 +45,13 @@ export class HistoryDialog extends React.PureComponent<HistoryDialogProps, Histo
<> <>
<span className="history-dialog-title">History</span> <span className="history-dialog-title">History</span>
<div className="history-record-entries"> <div className="history-record-entries">
{historyRecords.map((record: any) => { {historyRecords.map((record, i) => {
const auditInfo = record.auditInfo; const auditInfo = record.auditInfo;
const auditTime = record.auditTime; const auditTime = record.auditTime;
const formattedTime = auditTime.replace('T', ' ').substring(0, auditTime.length - 5); const formattedTime = auditTime.replace('T', ' ').substring(0, auditTime.length - 5);
return ( return (
<div key={record.auditTime} className="history-record-entry"> <div key={i} className="history-record-entry">
<Card> <Card>
<div className="history-record-title"> <div className="history-record-title">
<span className="history-record-title-change">Change</span> <span className="history-record-title-change">Change</span>

View File

@ -18,10 +18,10 @@
export * from './about-dialog/about-dialog'; export * from './about-dialog/about-dialog';
export * from './async-action-dialog/async-action-dialog'; export * from './async-action-dialog/async-action-dialog';
export * from './compaction-dialog/compaction-dialog'; export * from './compaction-dialog/compaction-dialog';
export * from './coordinator-dynamic-config/coordinator-dynamic-config'; export * from './coordinator-dynamic-config-dialog/coordinator-dynamic-config-dialog';
export * from './history-dialog/history-dialog'; export * from './history-dialog/history-dialog';
export * from './lookup-edit-dialog/lookup-edit-dialog'; export * from './lookup-edit-dialog/lookup-edit-dialog';
export * from './overlord-dynamic-config/overlord-dynamic-config'; export * from './overlord-dynamic-config-dialog/overlord-dynamic-config-dialog';
export * from './query-plan-dialog/query-plan-dialog'; export * from './query-plan-dialog/query-plan-dialog';
export * from './retention-dialog/retention-dialog'; export * from './retention-dialog/retention-dialog';
export * from './snitch-dialog/snitch-dialog'; export * from './snitch-dialog/snitch-dialog';

View File

@ -38,7 +38,7 @@ describe('lookup edit dialog', () => {
/> />
); );
const { container } = render(lookupEditDialog, { container: document.body }); render(lookupEditDialog);
expect(container.firstChild).toMatchSnapshot(); expect(document.body.lastChild).toMatchSnapshot();
}); });
}); });

View File

@ -32,7 +32,7 @@ import { validJson } from '../../utils';
import './lookup-edit-dialog.scss'; import './lookup-edit-dialog.scss';
export interface LookupEditDialogProps extends React.Props<any> { export interface LookupEditDialogProps {
isOpen: boolean; isOpen: boolean;
onClose: () => void; onClose: () => void;
onSubmit: () => void; onSubmit: () => void;
@ -105,7 +105,6 @@ export class LookupEditDialog extends React.PureComponent<
lookupVersion, lookupVersion,
onChange, onChange,
isEdit, isEdit,
allLookupTiers,
} = this.props; } = this.props;
const disableSubmit = const disableSubmit =
@ -155,8 +154,6 @@ export class LookupEditDialog extends React.PureComponent<
value={lookupSpec} value={lookupSpec}
editorProps={{ $blockScrolling: Infinity }} editorProps={{ $blockScrolling: Infinity }}
setOptions={{ setOptions={{
enableBasicAutocompletion: false,
enableLiveAutocompletion: false,
tabSize: 2, tabSize: 2,
}} }}
style={{}} style={{}}

View File

@ -0,0 +1,114 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`overload dynamic config 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"
tabindex="0"
/>
<div
class="bp3-dialog-container bp3-overlay-content bp3-overlay-appear bp3-overlay-appear-active"
tabindex="0"
>
<div
class="bp3-dialog snitch-dialog overlord-dynamic-config-dialog"
>
<div
class="bp3-dialog-header"
>
<h4
class="bp3-heading"
>
Overlord dynamic config
</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 0 0-1.71-.71L10 8.59l-3.29-3.3a1.003 1.003 0 0 0-1.42 1.42L8.59 10 5.3 13.29c-.19.18-.3.43-.3.71a1.003 1.003 0 0 0 1.71.71l3.29-3.3 3.29 3.29c.18.19.43.3.71.3a1.003 1.003 0 0 0 .71-1.71L11.41 10z"
fill-rule="evenodd"
/>
</svg>
</span>
</button>
</div>
<div
class="bp3-dialog-body"
>
<p>
Edit the overlord dynamic configuration on the fly. For more information please refer to the
<a
href="https://druid.apache.org/docs/latest/configuration/index.html#overlord-dynamic-configuration"
target="_blank"
>
documentation
</a>
.
</p>
<div
class="auto-form"
/>
</div>
<div
class="bp3-dialog-footer"
>
<div
class="bp3-dialog-footer-actions"
>
<button
class="bp3-button bp3-intent-primary"
type="button"
>
<span
class="bp3-button-text"
>
Next
</span>
<span
class="bp3-icon bp3-icon-arrow-right"
icon="arrow-right"
>
<svg
data-icon="arrow-right"
height="16"
viewBox="0 0 16 16"
width="16"
>
<desc>
arrow-right
</desc>
<path
d="M14.7 7.29l-5-5a.965.965 0 0 0-.71-.3 1.003 1.003 0 0 0-.71 1.71l3.29 3.29H1.99c-.55 0-1 .45-1 1s.45 1 1 1h9.59l-3.29 3.29a1.003 1.003 0 0 0 1.42 1.42l5-5c.18-.18.29-.43.29-.71s-.12-.52-.3-.7z"
fill-rule="evenodd"
/>
</svg>
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
`;

View File

@ -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 React from 'react';
import { render } from 'react-testing-library';
import { OverlordDynamicConfigDialog } from './overlord-dynamic-config-dialog';
describe('overload dynamic config', () => {
it('matches snapshot', () => {
const lookupEditDialog = <OverlordDynamicConfigDialog onClose={() => null} />;
render(lookupEditDialog);
expect(document.body.lastChild).toMatchSnapshot();
});
});

View File

@ -16,7 +16,7 @@
* limitations under the License. * limitations under the License.
*/ */
.overlord-dynamic-config { .overlord-dynamic-config-dialog {
&.bp3-dialog { &.bp3-dialog {
margin-top: 5vh; margin-top: 5vh;
top: 5%; top: 5%;

View File

@ -26,9 +26,9 @@ import { AppToaster } from '../../singletons/toaster';
import { getDruidErrorMessage, QueryManager } from '../../utils'; import { getDruidErrorMessage, QueryManager } from '../../utils';
import { SnitchDialog } from '../snitch-dialog/snitch-dialog'; import { SnitchDialog } from '../snitch-dialog/snitch-dialog';
import './overlord-dynamic-config.scss'; import './overlord-dynamic-config-dialog.scss';
export interface OverlordDynamicConfigDialogProps extends React.Props<any> { export interface OverlordDynamicConfigDialogProps {
onClose: () => void; onClose: () => void;
} }
@ -51,22 +51,22 @@ export class OverlordDynamicConfigDialog extends React.PureComponent<
allJSONValid: true, allJSONValid: true,
historyRecords: [], historyRecords: [],
}; };
}
componentDidMount() {
this.getConfig();
this.historyQueryManager = new QueryManager({ this.historyQueryManager = new QueryManager({
processQuery: async query => { processQuery: async () => {
const historyResp = await axios(`/druid/indexer/v1/worker/history?count=100`); const historyResp = await axios(`/druid/indexer/v1/worker/history?count=100`);
return historyResp.data; return historyResp.data;
}, },
onStateChange: ({ result, loading, error }) => { onStateChange: ({ result }) => {
this.setState({ this.setState({
historyRecords: result, historyRecords: result,
}); });
}, },
}); });
}
componentDidMount() {
this.getConfig();
this.historyQueryManager.runQuery(`dummy`); this.historyQueryManager.runQuery(`dummy`);
} }
@ -120,7 +120,7 @@ export class OverlordDynamicConfigDialog extends React.PureComponent<
return ( return (
<SnitchDialog <SnitchDialog
className="overlord-dynamic-config" className="overlord-dynamic-config-dialog"
isOpen isOpen
onSave={this.saveConfig} onSave={this.saveConfig}
onClose={onClose} onClose={onClose}

View File

@ -1,356 +0,0 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`overload dynamic config 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"
tabindex="0"
/>
<div
class="bp3-dialog-container bp3-overlay-content bp3-overlay-appear bp3-overlay-appear-active"
tabindex="0"
>
<div
class="bp3-dialog lookup-edit-dialog"
>
<div
class="bp3-dialog-header"
>
<h4
class="bp3-heading"
>
Add lookup
</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 0 0-1.71-.71L10 8.59l-3.29-3.3a1.003 1.003 0 0 0-1.42 1.42L8.59 10 5.3 13.29c-.19.18-.3.43-.3.71a1.003 1.003 0 0 0 1.71.71l3.29-3.3 3.29 3.29c.18.19.43.3.71.3a1.003 1.003 0 0 0 .71-1.71L11.41 10z"
fill-rule="evenodd"
/>
</svg>
</span>
</button>
</div>
<div
class="bp3-form-group lookup-label"
>
<label
class="bp3-label"
>
Name:
<span
class="bp3-text-muted"
/>
</label>
<div
class="bp3-form-content"
>
<div
class="bp3-input-group"
>
<input
class="bp3-input"
placeholder="Enter the lookup name"
style="padding-right: 10px;"
type="text"
value="test"
/>
</div>
</div>
</div>
<div
class="bp3-form-group lookup-label"
>
<label
class="bp3-label"
>
Tier:
<span
class="bp3-text-muted"
/>
</label>
<div
class="bp3-form-content"
>
<div
class="bp3-html-select"
>
<select>
<option
value="a"
>
a
</option>
<option
value="b"
>
b
</option>
<option
value="c"
>
c
</option>
<option
value="d"
>
d
</option>
<option
value="e"
>
e
</option>
<option
value="f"
>
f
</option>
<option
value="g"
>
g
</option>
<option
value="h"
>
h
</option>
<option
value="i"
>
i
</option>
<option
value="j"
>
j
</option>
</select>
<span
class="bp3-icon bp3-icon-double-caret-vertical"
icon="double-caret-vertical"
>
<svg
data-icon="double-caret-vertical"
height="16"
viewBox="0 0 16 16"
width="16"
>
<desc>
double-caret-vertical
</desc>
<path
d="M5 7h6a1.003 1.003 0 0 0 .71-1.71l-3-3C8.53 2.11 8.28 2 8 2s-.53.11-.71.29l-3 3A1.003 1.003 0 0 0 5 7zm6 2H5a1.003 1.003 0 0 0-.71 1.71l3 3c.18.18.43.29.71.29s.53-.11.71-.29l3-3A1.003 1.003 0 0 0 11 9z"
fill-rule="evenodd"
/>
</svg>
</span>
</div>
</div>
</div>
<div
class="bp3-form-group lookup-label"
>
<label
class="bp3-label"
>
Version:
<span
class="bp3-text-muted"
/>
</label>
<div
class="bp3-form-content"
>
<div
class="bp3-input-group"
>
<input
class="bp3-input"
placeholder="Enter the lookup version"
style="padding-right: 0px;"
type="text"
value="test"
/>
<span
class="bp3-input-action"
>
<button
class="bp3-button bp3-minimal"
type="button"
>
<span
class="bp3-button-text"
>
Use ISO as version
</span>
</button>
</span>
</div>
</div>
</div>
<div
class="bp3-form-group lookup-label"
>
<label
class="bp3-label"
>
Spec:
<span
class="bp3-text-muted"
/>
</label>
<div
class="bp3-form-content"
/>
</div>
<div
class=" ace_editor ace-tm lookup-edit-dialog-textarea"
id="brace-editor"
style="width: auto; height: 40vh;"
>
<textarea
autocapitalize="off"
autocorrect="off"
class="ace_text-input"
spellcheck="false"
style="opacity: 0;"
wrap="off"
/>
<div
aria-hidden="true"
class="ace_gutter"
style="display: none;"
>
<div
class="ace_layer ace_gutter-layer ace_folding-enabled"
/>
<div
class="ace_gutter-active-line"
/>
</div>
<div
class="ace_scroller"
>
<div
class="ace_content"
>
<div
class="ace_layer ace_print-margin-layer"
>
<div
class="ace_print-margin"
style="left: 4px; visibility: hidden;"
/>
</div>
<div
class="ace_layer ace_marker-layer"
/>
<div
class="ace_layer ace_text-layer"
style="padding: 0px 4px;"
/>
<div
class="ace_layer ace_marker-layer"
/>
<div
class="ace_layer ace_cursor-layer ace_hidden-cursors"
>
<div
class="ace_cursor"
/>
</div>
</div>
</div>
<div
class="ace_scrollbar ace_scrollbar-v"
style="display: none; width: 20px;"
>
<div
class="ace_scrollbar-inner"
style="width: 20px;"
/>
</div>
<div
class="ace_scrollbar ace_scrollbar-h"
style="display: none; height: 20px;"
>
<div
class="ace_scrollbar-inner"
style="height: 20px;"
/>
</div>
<div
style="height: auto; width: auto; top: 0px; left: 0px; visibility: hidden; position: absolute; white-space: pre; overflow: hidden;"
>
<div
style="height: auto; width: auto; top: 0px; left: 0px; visibility: hidden; position: absolute; white-space: pre; overflow: visible;"
/>
<div
style="height: auto; width: auto; top: 0px; left: 0px; visibility: hidden; position: absolute; white-space: pre; overflow: visible;"
>
XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX
</div>
</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>
<button
class="bp3-button bp3-disabled bp3-intent-primary"
disabled=""
tabindex="-1"
type="button"
>
<span
class="bp3-button-text"
>
Submit
</span>
</button>
</div>
</div>
</div>
</div>
</div>
</div>
`;

View File

@ -30,7 +30,7 @@ describe('query plan dialog', () => {
onClose={() => null} onClose={() => null}
/> />
); );
const { container } = render(queryPlanDialog, { container: document.body }); render(queryPlanDialog);
expect(container.firstChild).toMatchSnapshot(); expect(document.body.lastChild).toMatchSnapshot();
}); });
}); });

View File

@ -23,7 +23,7 @@ import { BasicQueryExplanation, SemiJoinQueryExplanation } from '../../utils';
import './query-plan-dialog.scss'; import './query-plan-dialog.scss';
export interface QueryPlanDialogProps extends React.Props<any> { export interface QueryPlanDialogProps {
explainResult: BasicQueryExplanation | SemiJoinQueryExplanation | string | null; explainResult: BasicQueryExplanation | SemiJoinQueryExplanation | string | null;
explainError: Error | null; explainError: Error | null;
onClose: () => void; onClose: () => void;

View File

@ -116,16 +116,6 @@ exports[`retention dialog matches snapshot 1`] = `
<div <div
class="bp3-dialog-footer-actions" class="bp3-dialog-footer-actions"
> >
<button
class="bp3-button bp3-minimal left-align-button"
type="button"
>
<span
class="bp3-button-text"
>
History
</span>
</button>
<button <button
class="bp3-button" class="bp3-button"
type="button" type="button"

View File

@ -1,56 +0,0 @@
/*
* 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 React from 'react';
import { reorderArray } from './retention-dialog';
describe('reorderArray', () => {
it('works when nothing changes', () => {
const array = ['a', 'b', 'c', 'd', 'e'];
const newArray = reorderArray(array, 0, 0);
expect(newArray).toEqual(['a', 'b', 'c', 'd', 'e']);
expect(array).toEqual(['a', 'b', 'c', 'd', 'e']);
});
it('works upward', () => {
const array = ['a', 'b', 'c', 'd', 'e'];
let newArray = reorderArray(array, 2, 1);
expect(newArray).toEqual(['a', 'c', 'b', 'd', 'e']);
expect(array).toEqual(['a', 'b', 'c', 'd', 'e']);
newArray = reorderArray(array, 2, 0);
expect(newArray).toEqual(['c', 'a', 'b', 'd', 'e']);
expect(array).toEqual(['a', 'b', 'c', 'd', 'e']);
});
it('works downward', () => {
const array = ['a', 'b', 'c', 'd', 'e'];
let newArray = reorderArray(array, 2, 3);
expect(newArray).toEqual(['a', 'b', 'c', 'd', 'e']);
expect(array).toEqual(['a', 'b', 'c', 'd', 'e']);
newArray = reorderArray(array, 2, 4);
expect(newArray).toEqual(['a', 'b', 'd', 'c', 'e']);
expect(array).toEqual(['a', 'b', 'c', 'd', 'e']);
});
});

View File

@ -19,7 +19,7 @@
import React from 'react'; import React from 'react';
import { render } from 'react-testing-library'; import { render } from 'react-testing-library';
import { RetentionDialog } from './retention-dialog'; import { reorderArray, RetentionDialog } from './retention-dialog';
describe('retention dialog', () => { describe('retention dialog', () => {
it('matches snapshot', () => { it('matches snapshot', () => {
@ -33,7 +33,42 @@ describe('retention dialog', () => {
onSave={() => null} onSave={() => null}
/> />
); );
const { container } = render(retentionDialog, { container: document.body }); render(retentionDialog);
expect(container.firstChild).toMatchSnapshot(); expect(document.body.lastChild).toMatchSnapshot();
});
describe('reorderArray', () => {
it('works when nothing changes', () => {
const array = ['a', 'b', 'c', 'd', 'e'];
const newArray = reorderArray(array, 0, 0);
expect(newArray).toEqual(['a', 'b', 'c', 'd', 'e']);
expect(array).toEqual(['a', 'b', 'c', 'd', 'e']);
});
it('works upward', () => {
const array = ['a', 'b', 'c', 'd', 'e'];
let newArray = reorderArray(array, 2, 1);
expect(newArray).toEqual(['a', 'c', 'b', 'd', 'e']);
expect(array).toEqual(['a', 'b', 'c', 'd', 'e']);
newArray = reorderArray(array, 2, 0);
expect(newArray).toEqual(['c', 'a', 'b', 'd', 'e']);
expect(array).toEqual(['a', 'b', 'c', 'd', 'e']);
});
it('works downward', () => {
const array = ['a', 'b', 'c', 'd', 'e'];
let newArray = reorderArray(array, 2, 3);
expect(newArray).toEqual(['a', 'b', 'c', 'd', 'e']);
expect(array).toEqual(['a', 'b', 'c', 'd', 'e']);
newArray = reorderArray(array, 2, 4);
expect(newArray).toEqual(['a', 'b', 'd', 'c', 'e']);
expect(array).toEqual(['a', 'b', 'c', 'd', 'e']);
});
}); });
}); });

View File

@ -21,7 +21,7 @@ import { IconNames } from '@blueprintjs/icons';
import axios from 'axios'; import axios from 'axios';
import React from 'react'; import React from 'react';
import { Rule, RuleEditor } from '../../components'; import { RuleEditor } from '../../components';
import { QueryManager } from '../../utils'; import { QueryManager } from '../../utils';
import { SnitchDialog } from '../snitch-dialog/snitch-dialog'; import { SnitchDialog } from '../snitch-dialog/snitch-dialog';
@ -37,7 +37,7 @@ export function reorderArray<T>(items: T[], oldIndex: number, newIndex: number):
return newItems; return newItems;
} }
export interface RetentionDialogProps extends React.Props<any> { export interface RetentionDialogProps {
datasource: string; datasource: string;
rules: any[]; rules: any[];
tiers: string[]; tiers: string[];
@ -64,23 +64,23 @@ export class RetentionDialog extends React.PureComponent<
currentRules: props.rules, currentRules: props.rules,
historyRecords: [], historyRecords: [],
}; };
}
componentDidMount() {
const { datasource } = this.props;
this.historyQueryManager = new QueryManager({ this.historyQueryManager = new QueryManager({
processQuery: async query => { processQuery: async datasource => {
const historyResp = await axios(`/druid/coordinator/v1/rules/${datasource}/history`); const historyResp = await axios(`/druid/coordinator/v1/rules/${datasource}/history`);
return historyResp.data; return historyResp.data;
}, },
onStateChange: ({ result, loading, error }) => { onStateChange: ({ result }) => {
this.setState({ this.setState({
historyRecords: result, historyRecords: result,
}); });
}, },
}); });
}
this.historyQueryManager.runQuery(`dummy`); componentDidMount() {
const { datasource } = this.props;
this.historyQueryManager.runQuery(datasource);
} }
private save = (comment: string) => { private save = (comment: string) => {
@ -106,7 +106,7 @@ export class RetentionDialog extends React.PureComponent<
onDeleteRule = (index: number) => { onDeleteRule = (index: number) => {
const { currentRules } = this.state; const { currentRules } = this.state;
const newRules = (currentRules || []).filter((r, i) => i !== index); const newRules = (currentRules || []).filter((_r, i) => i !== index);
this.setState({ this.setState({
currentRules: newRules, currentRules: newRules,

View File

@ -148,7 +148,18 @@ exports[`task table action dialog matches snapshot 1`] = `
> >
<div <div
class="footer-actions-left" class="footer-actions-left"
/> >
<button
class="bp3-button"
type="button"
>
<span
class="bp3-button-text"
>
test
</span>
</button>
</div>
<div <div
class="bp3-dialog-footer-actions" class="bp3-dialog-footer-actions"
> >

View File

@ -21,19 +21,18 @@ import { render } from 'react-testing-library';
import { SegmentTableActionDialog } from './segment-table-action-dialog'; import { SegmentTableActionDialog } from './segment-table-action-dialog';
const basicAction = { title: 'test', onAction: () => null };
describe('task table action dialog', () => { describe('task table action dialog', () => {
it('matches snapshot', () => { it('matches snapshot', () => {
const taskTableActionDialog = ( const taskTableActionDialog = (
<SegmentTableActionDialog <SegmentTableActionDialog
dataSourceId="test" dataSourceId="test"
segmentId="test" segmentId="test"
actions={[]} actions={[{ title: 'test', onAction: () => null }]}
onClose={() => null} onClose={() => null}
isOpen isOpen
/> />
); );
const { container } = render(taskTableActionDialog, { container: document.body }); render(taskTableActionDialog);
expect(container.firstChild).toMatchSnapshot(); expect(document.body.lastChild).toMatchSnapshot();
}); });
}); });

View File

@ -16,7 +16,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { IDialogProps, TextArea } from '@blueprintjs/core'; import { IDialogProps } from '@blueprintjs/core';
import React from 'react'; import React from 'react';
import { ShowJson } from '../../components'; import { ShowJson } from '../../components';

View File

@ -24,7 +24,7 @@ exports[`clipboard dialog matches snapshot 1`] = `
<h4 <h4
class="bp3-heading" class="bp3-heading"
> >
Show value Full value
</h4> </h4>
<button <button
aria-label="Close" aria-label="Close"

View File

@ -16,18 +16,17 @@
* limitations under the License. * limitations under the License.
*/ */
.show-value-dialog{ .show-value-dialog {
&.bp3-dialog{ &.bp3-dialog {
padding-bottom: 10px; padding-bottom: 10px;
} }
.bp3-input{ .bp3-input {
margin: 10px; margin: 10px;
height: 400px; height: 400px;
} }
.bp3-dialog-footer-actions{ .bp3-dialog-footer-actions {
padding-right: 10px; padding-right: 10px;
} }
} }

View File

@ -31,7 +31,7 @@ describe('clipboard dialog', () => {
} }
/> />
); );
const { container } = render(compactionDialog, { container: document.body }); render(compactionDialog);
expect(container.firstChild).toMatchSnapshot(); expect(document.body.lastChild).toMatchSnapshot();
}); });
}); });

View File

@ -15,16 +15,16 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
import { Button, Classes, Dialog, IconName, Intent, TextArea } from '@blueprintjs/core'; import { Button, Classes, Dialog, Intent, TextArea } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons'; import { IconNames } from '@blueprintjs/icons';
import copy = require('copy-to-clipboard'); import copy from 'copy-to-clipboard';
import React from 'react'; import React from 'react';
import { AppToaster } from '../../singletons/toaster'; import { AppToaster } from '../../singletons/toaster';
import './show-value-dialog.scss'; import './show-value-dialog.scss';
export interface ShowValueDialogProps extends React.Props<any> { export interface ShowValueDialogProps {
onClose: () => void; onClose: () => void;
str: string; str: string;
} }
@ -39,23 +39,22 @@ export class ShowValueDialog extends React.PureComponent<ShowValueDialogProps> {
const { onClose, str } = this.props; const { onClose, str } = this.props;
return ( return (
<Dialog className="show-value-dialog" isOpen onClose={onClose} title={'Show value'}> <Dialog className="show-value-dialog" isOpen onClose={onClose} title="Full value">
<TextArea value={str} /> <TextArea value={str} />
<div className={Classes.DIALOG_FOOTER_ACTIONS}> <div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button <Button icon={IconNames.DUPLICATE} text={'Copy'} onClick={this.handleCopy} />
icon={IconNames.DUPLICATE} <Button text={'Close'} intent={Intent.PRIMARY} onClick={onClose} />
text={'Copy'}
onClick={() => {
copy(str, { format: 'text/plain' });
AppToaster.show({
message: 'Value copied to clipboard',
intent: Intent.SUCCESS,
});
}}
/>
<Button text={'Close'} intent={'primary'} onClick={onClose} />
</div> </div>
</Dialog> </Dialog>
); );
} }
private handleCopy = () => {
const { str } = this.props;
copy(str, { format: 'text/plain' });
AppToaster.show({
message: 'Value copied to clipboard',
intent: Intent.SUCCESS,
});
};
} }

View File

@ -24,7 +24,7 @@ import { SnitchDialog } from './snitch-dialog';
describe('snitch dialog', () => { describe('snitch dialog', () => {
it('matches snapshot', () => { it('matches snapshot', () => {
const snitchDialog = <SnitchDialog onSave={() => null} isOpen />; const snitchDialog = <SnitchDialog onSave={() => null} isOpen />;
const { container } = render(snitchDialog, { container: document.body }); render(snitchDialog);
expect(container.firstChild).toMatchSnapshot(); expect(document.body.lastChild).toMatchSnapshot();
}); });
}); });

View File

@ -44,7 +44,6 @@ export interface SnitchDialogState {
comment: string; comment: string;
showFinalStep?: boolean; showFinalStep?: boolean;
saveDisabled?: boolean;
showHistory?: boolean; showHistory?: boolean;
} }
@ -55,7 +54,6 @@ export class SnitchDialog extends React.PureComponent<SnitchDialogProps, SnitchD
this.state = { this.state = {
comment: '', comment: '',
saveDisabled: true,
}; };
} }
@ -68,11 +66,8 @@ export class SnitchDialog extends React.PureComponent<SnitchDialogProps, SnitchD
}; };
changeComment(newComment: string) { changeComment(newComment: string) {
const { comment } = this.state;
this.setState({ this.setState({
comment: newComment, comment: newComment,
saveDisabled: !newComment,
}); });
} }
@ -95,15 +90,14 @@ export class SnitchDialog extends React.PureComponent<SnitchDialogProps, SnitchD
}); });
}; };
goToHistory = () => { handleGoToHistory = () => {
this.setState({ this.setState({
showHistory: true, showHistory: true,
}); });
}; };
renderFinalStep() { renderFinalStep() {
const { onClose, children } = this.props; const { comment } = this.state;
const { saveDisabled, comment } = this.state;
return ( return (
<Dialog {...this.props}> <Dialog {...this.props}>
@ -117,14 +111,14 @@ export class SnitchDialog extends React.PureComponent<SnitchDialogProps, SnitchD
/> />
</FormGroup> </FormGroup>
</div> </div>
<div className={Classes.DIALOG_FOOTER}>{this.renderActions(!comment)}</div>
<div className={Classes.DIALOG_FOOTER}>{this.renderActions(saveDisabled)}</div>
</Dialog> </Dialog>
); );
} }
renderHistoryDialog() { renderHistoryDialog() {
const { historyRecords } = this.props; const { historyRecords } = this.props;
if (!historyRecords) return;
return ( return (
<HistoryDialog {...this.props} className="history-dialog" historyRecords={historyRecords}> <HistoryDialog {...this.props} className="history-dialog" historyRecords={historyRecords}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}> <div className={Classes.DIALOG_FOOTER_ACTIONS}>
@ -142,8 +136,13 @@ export class SnitchDialog extends React.PureComponent<SnitchDialogProps, SnitchD
return ( return (
<div className={Classes.DIALOG_FOOTER_ACTIONS}> <div className={Classes.DIALOG_FOOTER_ACTIONS}>
{showFinalStep || historyRecords === undefined ? null : ( {!showFinalStep && historyRecords && (
<Button className="left-align-button" minimal text="History" onClick={this.goToHistory} /> <Button
className="left-align-button"
minimal
text="History"
onClick={this.handleGoToHistory}
/>
)} )}
{showFinalStep ? ( {showFinalStep ? (
@ -178,7 +177,7 @@ export class SnitchDialog extends React.PureComponent<SnitchDialogProps, SnitchD
} }
render() { render() {
const { onClose, className, children, saveDisabled } = this.props; const { children, saveDisabled } = this.props;
const { showFinalStep, showHistory } = this.state; const { showFinalStep, showHistory } = this.state;
if (showFinalStep) return this.renderFinalStep(); if (showFinalStep) return this.renderFinalStep();
@ -189,7 +188,6 @@ export class SnitchDialog extends React.PureComponent<SnitchDialogProps, SnitchD
return ( return (
<Dialog isOpen {...propsClone}> <Dialog isOpen {...propsClone}>
<div className={Classes.DIALOG_BODY}>{children}</div> <div className={Classes.DIALOG_BODY}>{children}</div>
<div className={Classes.DIALOG_FOOTER}>{this.renderActions(saveDisabled)}</div> <div className={Classes.DIALOG_FOOTER}>{this.renderActions(saveDisabled)}</div>
</Dialog> </Dialog>
); );

View File

@ -23,10 +23,8 @@ import { SpecDialog } from './spec-dialog';
describe('spec dialog', () => { describe('spec dialog', () => {
it('matches snapshot', () => { it('matches snapshot', () => {
const specDialog = ( const specDialog = <SpecDialog onSubmit={() => null} onClose={() => null} title={'test'} />;
<SpecDialog onSubmit={(spec: JSON) => null} onClose={() => null} title={'test'} /> render(specDialog);
); expect(document.body.lastChild).toMatchSnapshot();
const { container } = render(specDialog, { container: document.body });
expect(container.firstChild).toMatchSnapshot();
}); });
}); });

View File

@ -22,7 +22,7 @@ import AceEditor from 'react-ace';
import './spec-dialog.scss'; import './spec-dialog.scss';
export interface SpecDialogProps extends React.Props<any> { export interface SpecDialogProps {
onSubmit: (spec: JSON) => void; onSubmit: (spec: JSON) => void;
onClose: () => void; onClose: () => void;
title: string; title: string;
@ -84,10 +84,7 @@ export class SpecDialog extends React.PureComponent<SpecDialogProps, SpecDialogS
value={spec} value={spec}
width="100%" width="100%"
setOptions={{ setOptions={{
enableBasicAutocompletion: true,
enableLiveAutocompletion: true,
showLineNumbers: true, showLineNumbers: true,
enableSnippets: true,
tabSize: 2, tabSize: 2,
}} }}
style={{}} style={{}}

View File

@ -32,7 +32,7 @@ describe('supervisor table action dialog', () => {
isOpen isOpen
/> />
); );
const { container } = render(supervisorTableActionDialog, { container: document.body }); render(supervisorTableActionDialog);
expect(container.firstChild).toMatchSnapshot(); expect(document.body.lastChild).toMatchSnapshot();
}); });
}); });

View File

@ -75,7 +75,7 @@ $side-bar-width: 120px;
left: $side-bar-width; left: $side-bar-width;
right: 0; right: 0;
height: 100%; height: 100%;
padding: 10px 20px; padding: 10px 20px 15px 20px;
} }
} }

View File

@ -30,7 +30,7 @@ describe('table action dialog', () => {
isOpen isOpen
/> />
); );
const { container } = render(tableActionDialog, { container: document.body }); render(tableActionDialog);
expect(container.firstChild).toMatchSnapshot(); expect(document.body.lastChild).toMatchSnapshot();
}); });
}); });

View File

@ -47,11 +47,11 @@ export class TableActionDialog extends React.PureComponent<TableActionDialogProp
<Dialog className="table-action-dialog" isOpen={isOpen} onClose={onClose} title={title}> <Dialog className="table-action-dialog" isOpen={isOpen} onClose={onClose} title={title}>
<div className={Classes.DIALOG_BODY}> <div className={Classes.DIALOG_BODY}>
<div className="side-bar"> <div className="side-bar">
{sideButtonMetadata.map((d: SideButtonMetaData) => ( {sideButtonMetadata.map((d, i) => (
<Button <Button
className="tab-button" className="tab-button"
icon={<Icon icon={d.icon} iconSize={20} />} icon={<Icon icon={d.icon} iconSize={20} />}
key={d.text} key={i}
text={d.text} text={d.text}
intent={d.active ? Intent.PRIMARY : Intent.NONE} intent={d.active ? Intent.PRIMARY : Intent.NONE}
minimal={!d.active} minimal={!d.active}

View File

@ -33,7 +33,7 @@ describe('task table action dialog', () => {
isOpen isOpen
/> />
); );
const { container } = render(taskTableActionDialog, { container: document.body }); render(taskTableActionDialog);
expect(container.firstChild).toMatchSnapshot(); expect(document.body.lastChild).toMatchSnapshot();
}); });
}); });

View File

@ -16,7 +16,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { Button, HTMLSelect, InputGroup, Intent } from '@blueprintjs/core'; import { Button, HTMLSelect, InputGroup } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons'; import { IconNames } from '@blueprintjs/icons';
import FileSaver from 'file-saver'; import FileSaver from 'file-saver';
import hasOwnProp from 'has-own-prop'; import hasOwnProp from 'has-own-prop';
@ -232,11 +232,6 @@ export function pluralIfNeeded(n: number, singular: string, plural?: string): st
return `${formatNumber(n)} ${n === 1 ? singular : plural}`; return `${formatNumber(n)} ${n === 1 ? singular : plural}`;
} }
export function getHeadProp(results: Record<string, any>[], prop: string): any {
if (!results || !results.length) return null;
return results[0][prop] || null;
}
// ---------------------------- // ----------------------------
export function parseJson(json: string): any { export function parseJson(json: string): any {
@ -274,10 +269,6 @@ export function parseStringToJSON(s: string): JSON | null {
} }
} }
export function selectDefined<T, Q>(xs: (Q | null | undefined)[]): Q[] {
return xs.filter(Boolean) as any;
}
export function filterMap<T, Q>(xs: T[], f: (x: T, i?: number) => Q | null | undefined): Q[] { export function filterMap<T, Q>(xs: T[], f: (x: T, i?: number) => Q | null | undefined): Q[] {
return (xs.map(f) as any).filter(Boolean); return (xs.map(f) as any).filter(Boolean);
} }

View File

@ -17,7 +17,6 @@
*/ */
import { Code } from '@blueprintjs/core'; import { Code } from '@blueprintjs/core';
import { number } from 'prop-types';
import React from 'react'; import React from 'react';
import { Field } from '../components/auto-form/auto-form'; import { Field } from '../components/auto-form/auto-form';
@ -184,11 +183,12 @@ export interface ParseSpec {
export function hasParallelAbility(spec: IngestionSpec): boolean { export function hasParallelAbility(spec: IngestionSpec): boolean {
const specType = getSpecType(spec); const specType = getSpecType(spec);
return spec.type === 'index' || spec.type === 'index_parallel'; return specType === 'index' || specType === 'index_parallel';
} }
export function isParallel(spec: IngestionSpec): boolean { export function isParallel(spec: IngestionSpec): boolean {
return spec.type === 'index_parallel'; const specType = getSpecType(spec);
return specType === 'index_parallel';
} }
export type DimensionMode = 'specific' | 'auto-detect'; export type DimensionMode = 'specific' | 'auto-detect';
@ -1482,6 +1482,45 @@ const TUNING_CONFIG_FORM_FIELDS: Field<TuningConfig>[] = [
placeholder: 'Default: 1/6 of max JVM memory', placeholder: 'Default: 1/6 of max JVM memory',
info: <>Used in determining when intermediate persists to disk should occur.</>, info: <>Used in determining when intermediate persists to disk should occur.</>,
}, },
{
name: 'indexSpec.bitmap.type',
label: 'Index bitmap type',
type: 'string',
defaultValue: 'concise',
suggestions: ['concise', 'roaring'],
info: <>Compression format for bitmap indexes.</>,
},
{
name: 'indexSpec.dimensionCompression',
label: 'Index dimension compression',
type: 'string',
defaultValue: 'lz4',
suggestions: ['lz4', 'lzf', 'uncompressed'],
info: <>Compression format for dimension columns.</>,
},
{
name: 'indexSpec.metricCompression',
label: 'Index metric compression',
type: 'string',
defaultValue: 'lz4',
suggestions: ['lz4', 'lzf', 'uncompressed'],
info: <>Compression format for metric columns.</>,
},
{
name: 'indexSpec.longEncoding',
label: 'Index long encoding',
type: 'string',
defaultValue: 'longs',
suggestions: ['longs', 'auto'],
info: (
<>
Encoding format for long-typed columns. Applies regardless of whether they are dimensions or
metrics. <Code>auto</Code> encodes the values using offset or lookup table depending on
column cardinality, and store them with variable size. <Code>longs</Code> stores the value
as-is with 8 bytes each.
</>
),
},
{ {
name: 'intermediatePersistPeriod', name: 'intermediatePersistPeriod',
type: 'duration', type: 'duration',
@ -1513,10 +1552,6 @@ const TUNING_CONFIG_FORM_FIELDS: Field<TuningConfig>[] = [
</> </>
), ),
}, },
{
name: 'forceExtendableShardSpecs',
type: 'boolean',
},
{ {
name: 'pushTimeout', name: 'pushTimeout',
type: 'number', type: 'number',

View File

@ -25,24 +25,21 @@ export class LocalStorageBackedArray<T> {
constructor(key: LocalStorageKeys, array?: T[]) { constructor(key: LocalStorageKeys, array?: T[]) {
this.key = key; this.key = key;
if (!Array.isArray(array)) { if (!Array.isArray(array)) {
this.getDataFromStorage(); this.storedArray = this.getDataFromStorage();
} else { } else {
this.storedArray = array; this.storedArray = array;
this.setDataInStorage(); this.setDataInStorage();
} }
} }
private getDataFromStorage(): void { private getDataFromStorage(): T[] {
let possibleArray: any;
try { try {
possibleArray = JSON.parse(String(localStorageGet(this.key))); const possibleArray: any = JSON.parse(String(localStorageGet(this.key)));
if (!Array.isArray(possibleArray)) return [];
return possibleArray;
} catch { } catch {
// show all columns by default return [];
possibleArray = [];
} }
if (!Array.isArray(possibleArray)) possibleArray = [];
this.storedArray = possibleArray;
} }
private setDataInStorage(): void { private setDataInStorage(): void {

View File

@ -32,6 +32,7 @@ export const LocalStorageKeys = {
SEGMENTS_REFRESH_RATE: 'segments-refresh-rate' as 'segments-refresh-rate', SEGMENTS_REFRESH_RATE: 'segments-refresh-rate' as 'segments-refresh-rate',
SERVERS_REFRESH_RATE: 'servers-refresh-rate' as 'servers-refresh-rate', SERVERS_REFRESH_RATE: 'servers-refresh-rate' as 'servers-refresh-rate',
SUPERVISORS_REFRESH_RATE: 'supervisors-refresh-rate' as 'supervisors-refresh-rate', SUPERVISORS_REFRESH_RATE: 'supervisors-refresh-rate' as 'supervisors-refresh-rate',
LOOKUPS_REFRESH_RATE: 'lookups-refresh-rate' as 'lookups-refresh-rate',
}; };
export type LocalStorageKeys = typeof LocalStorageKeys[keyof typeof LocalStorageKeys]; export type LocalStorageKeys = typeof LocalStorageKeys[keyof typeof LocalStorageKeys];

View File

@ -36,8 +36,8 @@ export class QueryManager<Q, R> {
private onStateChange?: (queryResolve: QueryStateInt<R>) => void; private onStateChange?: (queryResolve: QueryStateInt<R>) => void;
private terminated = false; private terminated = false;
private nextQuery: Q; private nextQuery: Q | undefined;
private lastQuery: Q; private lastQuery: Q | undefined;
private actuallyLoading = false; private actuallyLoading = false;
private state: QueryStateInt<R> = { private state: QueryStateInt<R> = {
result: null, result: null,
@ -73,6 +73,7 @@ export class QueryManager<Q, R> {
private run() { private run() {
this.lastQuery = this.nextQuery; this.lastQuery = this.nextQuery;
if (typeof this.lastQuery === 'undefined') return;
this.currentQueryId++; this.currentQueryId++;
const myQueryId = this.currentQueryId; const myQueryId = this.currentQueryId;
@ -134,7 +135,7 @@ export class QueryManager<Q, R> {
} }
} }
public getLastQuery(): Q { public getLastQuery(): Q | undefined {
return this.lastQuery; return this.lastQuery;
} }

View File

@ -26,6 +26,7 @@ exports[`data source view matches snapshot 1`] = `
Array [ Array [
"Datasource", "Datasource",
"Availability", "Availability",
"Segment load/drop",
"Retention", "Retention",
"Compaction", "Compaction",
"Size", "Size",
@ -110,6 +111,14 @@ exports[`data source view matches snapshot 1`] = `
"show": true, "show": true,
"sortMethod": [Function], "sortMethod": [Function],
}, },
Object {
"Cell": [Function],
"Header": "Segment load/drop",
"accessor": "num_segments_to_load",
"filterable": false,
"id": "load-drop",
"show": true,
},
Object { Object {
"Cell": [Function], "Cell": [Function],
"Header": "Retention", "Header": "Retention",

View File

@ -24,11 +24,7 @@ import { DatasourcesView } from './datasource-view';
describe('data source view', () => { describe('data source view', () => {
it('matches snapshot', () => { it('matches snapshot', () => {
const dataSourceView = shallow( const dataSourceView = shallow(
<DatasourcesView <DatasourcesView goToQuery={() => {}} goToSegments={() => {}} noSqlMode={false} />,
goToQuery={(initSql: string) => {}}
goToSegments={(datasource: string, onlyUnavailable?: boolean) => {}}
noSqlMode={false}
/>,
); );
expect(dataSourceView).toMatchSnapshot(); expect(dataSourceView).toMatchSnapshot();
}); });

View File

@ -16,16 +16,7 @@
* limitations under the License. * limitations under the License.
*/ */
import { import { Button, FormGroup, InputGroup, Intent, Switch } from '@blueprintjs/core';
Button,
FormGroup,
Icon,
InputGroup,
Intent,
Popover,
Position,
Switch,
} from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons'; import { IconNames } from '@blueprintjs/icons';
import axios from 'axios'; import axios from 'axios';
import React from 'react'; import React from 'react';
@ -55,12 +46,14 @@ import {
} from '../../utils'; } from '../../utils';
import { BasicAction } from '../../utils/basic-action'; import { BasicAction } from '../../utils/basic-action';
import { LocalStorageBackedArray } from '../../utils/local-storage-backed-array'; import { LocalStorageBackedArray } from '../../utils/local-storage-backed-array';
import { deepGet } from '../../utils/object-change';
import './datasource-view.scss'; import './datasource-view.scss';
const tableColumns: string[] = [ const tableColumns: string[] = [
'Datasource', 'Datasource',
'Availability', 'Availability',
'Segment load/drop',
'Retention', 'Retention',
'Compaction', 'Compaction',
'Size', 'Size',
@ -70,13 +63,25 @@ const tableColumns: string[] = [
const tableColumnsNoSql: string[] = [ const tableColumnsNoSql: string[] = [
'Datasource', 'Datasource',
'Availability', 'Availability',
'Segment load/drop',
'Retention', 'Retention',
'Compaction', 'Compaction',
'Size', 'Size',
ActionCell.COLUMN_LABEL, ActionCell.COLUMN_LABEL,
]; ];
export interface DatasourcesViewProps extends React.Props<any> { function formatLoadDrop(segmentsToLoad: number, segmentsToDrop: number): string {
const loadDrop: string[] = [];
if (segmentsToLoad) {
loadDrop.push(`${segmentsToLoad} segments to load`);
}
if (segmentsToDrop) {
loadDrop.push(`${segmentsToDrop} segments to drop`);
}
return loadDrop.join(', ') || 'No segments to load/drop';
}
export interface DatasourcesViewProps {
goToQuery: (initSql: string) => void; goToQuery: (initSql: string) => void;
goToSegments: (datasource: string, onlyUnavailable?: boolean) => void; goToSegments: (datasource: string, onlyUnavailable?: boolean) => void;
noSqlMode: boolean; noSqlMode: boolean;
@ -90,10 +95,12 @@ interface Datasource {
interface DatasourceQueryResultRow { interface DatasourceQueryResultRow {
datasource: string; datasource: string;
num_available_segments: number;
num_rows: number;
num_segments: number; num_segments: number;
num_available_segments: number;
num_segments_to_load: number;
num_segments_to_drop: number;
size: number; size: number;
num_rows: number;
} }
export interface DatasourcesViewState { export interface DatasourcesViewState {
@ -124,6 +131,17 @@ export class DatasourcesView extends React.PureComponent<
static FULLY_AVAILABLE_COLOR = '#57d500'; static FULLY_AVAILABLE_COLOR = '#57d500';
static PARTIALLY_AVAILABLE_COLOR = '#ffbf00'; static PARTIALLY_AVAILABLE_COLOR = '#ffbf00';
static DATASOURCE_SQL = `SELECT
datasource,
COUNT(*) FILTER (WHERE (is_published = 1 AND is_overshadowed = 0) OR is_realtime = 1) AS num_segments,
COUNT(*) FILTER (WHERE is_available = 1 AND ((is_published = 1 AND is_overshadowed = 0) OR is_realtime = 1)) AS num_available_segments,
COUNT(*) FILTER (WHERE is_published = 1 AND is_overshadowed = 0 AND is_available = 0) AS num_segments_to_load,
COUNT(*) FILTER (WHERE is_available = 1 AND NOT ((is_published = 1 AND is_overshadowed = 0) OR is_realtime = 1)) AS num_segments_to_drop,
SUM("size") FILTER (WHERE (is_published = 1 AND is_overshadowed = 0) OR is_realtime = 1) AS size,
SUM("num_rows") FILTER (WHERE (is_published = 1 AND is_overshadowed = 0) OR is_realtime = 1) AS num_rows
FROM sys.segments
GROUP BY 1`;
static formatRules(rules: any[]): string { static formatRules(rules: any[]): string {
if (rules.length === 0) { if (rules.length === 0) {
return 'No rules'; return 'No rules';
@ -135,7 +153,7 @@ export class DatasourcesView extends React.PureComponent<
} }
private datasourceQueryManager: QueryManager< private datasourceQueryManager: QueryManager<
string, boolean,
{ tiers: string[]; defaultRules: any[]; datasources: Datasource[] } { tiers: string[]; defaultRules: any[]; datasources: Datasource[] }
>; >;
@ -162,30 +180,31 @@ export class DatasourcesView extends React.PureComponent<
LocalStorageKeys.DATASOURCE_TABLE_COLUMN_SELECTION, LocalStorageKeys.DATASOURCE_TABLE_COLUMN_SELECTION,
), ),
}; };
}
componentDidMount(): void {
const { noSqlMode } = this.props;
const { hiddenColumns } = this.state;
this.datasourceQueryManager = new QueryManager({ this.datasourceQueryManager = new QueryManager({
processQuery: async (query: string) => { processQuery: async noSqlMode => {
let datasources: DatasourceQueryResultRow[]; let datasources: DatasourceQueryResultRow[];
if (!noSqlMode) { if (!noSqlMode) {
datasources = await queryDruidSql({ query }); datasources = await queryDruidSql({ query: DatasourcesView.DATASOURCE_SQL });
} else { } else {
const datasourcesResp = await axios.get('/druid/coordinator/v1/datasources?simple'); const datasourcesResp = await axios.get('/druid/coordinator/v1/datasources?simple');
const loadstatusResp = await axios.get('/druid/coordinator/v1/loadstatus?simple'); const loadstatusResp = await axios.get('/druid/coordinator/v1/loadstatus?simple');
const loadstatus = loadstatusResp.data; const loadstatus = loadstatusResp.data;
datasources = datasourcesResp.data.map((d: any) => { datasources = datasourcesResp.data.map(
return { (d: any): DatasourceQueryResultRow => {
datasource: d.name, const segmentsToLoad = Number(loadstatus[d.name] || 0);
num_available_segments: d.properties.segments.count, const availableSegments = Number(deepGet(d, 'properties.segments.count'));
size: d.properties.segments.size, return {
num_segments: d.properties.segments.count + loadstatus[d.name], datasource: d.name,
num_rows: -1, num_available_segments: availableSegments,
}; num_segments: availableSegments + segmentsToLoad,
}); num_segments_to_load: segmentsToLoad,
num_segments_to_drop: 0,
size: d.properties.segments.size,
num_rows: -1,
};
},
);
} }
const seen = countBy(datasources, (x: any) => x.datasource); const seen = countBy(datasources, (x: any) => x.datasource);
@ -234,15 +253,11 @@ export class DatasourcesView extends React.PureComponent<
}); });
}, },
}); });
}
this.datasourceQueryManager.runQuery(`SELECT componentDidMount(): void {
datasource, const { noSqlMode } = this.props;
COUNT(*) AS num_segments, this.datasourceQueryManager.runQuery(noSqlMode);
SUM(is_available) AS num_available_segments,
SUM("size") AS size,
SUM("num_rows") AS num_rows
FROM sys.segments
GROUP BY 1`);
} }
componentWillUnmount(): void { componentWillUnmount(): void {
@ -581,7 +596,7 @@ GROUP BY 1`);
} }
filterable filterable
filtered={datasourcesFilter} filtered={datasourcesFilter}
onFilteredChange={(filtered, column) => { onFilteredChange={filtered => {
this.setState({ datasourcesFilter: filtered }); this.setState({ datasourcesFilter: filtered });
}} }}
columns={[ columns={[
@ -669,6 +684,17 @@ GROUP BY 1`);
}, },
show: hiddenColumns.exists('Availability'), show: hiddenColumns.exists('Availability'),
}, },
{
Header: 'Segment load/drop',
id: 'load-drop',
accessor: 'num_segments_to_load',
filterable: false,
Cell: row => {
const { num_segments_to_load, num_segments_to_drop } = row.original;
return formatLoadDrop(num_segments_to_load, num_segments_to_drop);
},
show: hiddenColumns.exists('Segment load/drop'),
},
{ {
Header: 'Retention', Header: 'Retention',
id: 'retention', id: 'retention',
@ -789,7 +815,7 @@ GROUP BY 1`);
<Button <Button
icon={IconNames.APPLICATION} icon={IconNames.APPLICATION}
text="Go to SQL" text="Go to SQL"
onClick={() => goToQuery(this.datasourceQueryManager.getLastQuery())} onClick={() => goToQuery(DatasourcesView.DATASOURCE_SQL)}
/> />
)} )}
<Switch <Switch

View File

@ -19,10 +19,12 @@
import { Card, H5, Icon } from '@blueprintjs/core'; import { Card, H5, Icon } from '@blueprintjs/core';
import { IconName, IconNames } from '@blueprintjs/icons'; import { IconName, IconNames } from '@blueprintjs/icons';
import axios from 'axios'; import axios from 'axios';
import { sum } from 'd3-array';
import React from 'react'; import React from 'react';
import { UrlBaser } from '../../singletons/url-baser'; import { UrlBaser } from '../../singletons/url-baser';
import { getHeadProp, lookupBy, pluralIfNeeded, queryDruidSql, QueryManager } from '../../utils'; import { lookupBy, pluralIfNeeded, queryDruidSql, QueryManager } from '../../utils';
import { deepGet } from '../../utils/object-change';
import './home-view.scss'; import './home-view.scss';
@ -35,7 +37,7 @@ export interface CardOptions {
error?: string | null; error?: string | null;
} }
export interface HomeViewProps extends React.Props<any> { export interface HomeViewProps {
noSqlMode: boolean; noSqlMode: boolean;
} }
@ -50,6 +52,7 @@ export interface HomeViewState {
segmentCountLoading: boolean; segmentCountLoading: boolean;
segmentCount: number; segmentCount: number;
unavailableSegmentCount: number;
segmentCountError: string | null; segmentCountError: string | null;
supervisorCountLoading: boolean; supervisorCountLoading: boolean;
@ -77,12 +80,12 @@ export interface HomeViewState {
} }
export class HomeView extends React.PureComponent<HomeViewProps, HomeViewState> { export class HomeView extends React.PureComponent<HomeViewProps, HomeViewState> {
private statusQueryManager: QueryManager<string, any>; private statusQueryManager: QueryManager<null, any>;
private datasourceQueryManager: QueryManager<string, any>; private datasourceQueryManager: QueryManager<boolean, any>;
private segmentQueryManager: QueryManager<string, any>; private segmentQueryManager: QueryManager<boolean, any>;
private supervisorQueryManager: QueryManager<string, any>; private supervisorQueryManager: QueryManager<null, any>;
private taskQueryManager: QueryManager<string, any>; private taskQueryManager: QueryManager<boolean, any>;
private serverQueryManager: QueryManager<string, any>; private serverQueryManager: QueryManager<boolean, any>;
constructor(props: HomeViewProps, context: any) { constructor(props: HomeViewProps, context: any) {
super(props, context); super(props, context);
@ -97,6 +100,7 @@ export class HomeView extends React.PureComponent<HomeViewProps, HomeViewState>
segmentCountLoading: false, segmentCountLoading: false,
segmentCount: 0, segmentCount: 0,
unavailableSegmentCount: 0,
segmentCountError: null, segmentCountError: null,
supervisorCountLoading: false, supervisorCountLoading: false,
@ -122,13 +126,9 @@ export class HomeView extends React.PureComponent<HomeViewProps, HomeViewState>
peonCount: 0, peonCount: 0,
serverCountError: null, serverCountError: null,
}; };
}
componentDidMount(): void {
const { noSqlMode } = this.props;
this.statusQueryManager = new QueryManager({ this.statusQueryManager = new QueryManager({
processQuery: async query => { processQuery: async () => {
const statusResp = await axios.get('/status'); const statusResp = await axios.get('/status');
return statusResp.data; return statusResp.data;
}, },
@ -141,15 +141,13 @@ export class HomeView extends React.PureComponent<HomeViewProps, HomeViewState>
}, },
}); });
this.statusQueryManager.runQuery(`dummy`);
// -------------------------
this.datasourceQueryManager = new QueryManager({ this.datasourceQueryManager = new QueryManager({
processQuery: async query => { processQuery: async noSqlMode => {
let datasources: string[]; let datasources: string[];
if (!noSqlMode) { if (!noSqlMode) {
datasources = await queryDruidSql({ query }); datasources = await queryDruidSql({
query: `SELECT datasource FROM sys.segments GROUP BY 1`,
});
} else { } else {
const datasourcesResp = await axios.get('/druid/coordinator/v1/datasources'); const datasourcesResp = await axios.get('/druid/coordinator/v1/datasources');
datasources = datasourcesResp.data; datasources = datasourcesResp.data;
@ -165,45 +163,45 @@ export class HomeView extends React.PureComponent<HomeViewProps, HomeViewState>
}, },
}); });
this.datasourceQueryManager.runQuery(`SELECT datasource FROM sys.segments GROUP BY 1`);
// -------------------------
this.segmentQueryManager = new QueryManager({ this.segmentQueryManager = new QueryManager({
processQuery: async query => { processQuery: async noSqlMode => {
if (noSqlMode) { if (noSqlMode) {
const loadstatusResp = await axios.get('/druid/coordinator/v1/loadstatus?simple'); const loadstatusResp = await axios.get('/druid/coordinator/v1/loadstatus?simple');
const loadstatus = loadstatusResp.data; const loadstatus = loadstatusResp.data;
const unavailableSegmentNum = Object.keys(loadstatus).reduce((sum, key) => { const unavailableSegmentNum = sum(Object.keys(loadstatus), key => loadstatus[key]);
return sum + loadstatus[key];
}, 0);
const datasourcesMetaResp = await axios.get('/druid/coordinator/v1/datasources?simple'); const datasourcesMetaResp = await axios.get('/druid/coordinator/v1/datasources?simple');
const datasourcesMeta = datasourcesMetaResp.data; const datasourcesMeta = datasourcesMetaResp.data;
const availableSegmentNum = datasourcesMeta.reduce((sum: number, curr: any) => { const availableSegmentNum = sum(datasourcesMeta, (curr: any) =>
return sum + curr.properties.segments.count; deepGet(curr, 'properties.segments.count'),
}, 0); );
return availableSegmentNum + unavailableSegmentNum; return {
count: availableSegmentNum + unavailableSegmentNum,
unavailable: unavailableSegmentNum,
};
} else { } else {
const segments = await queryDruidSql({ query }); const segments = await queryDruidSql({
return getHeadProp(segments, 'count') || 0; query: `SELECT
COUNT(*) as "count",
COUNT(*) FILTER (WHERE is_available = 0) as "unavailable"
FROM sys.segments`,
});
return segments.length === 1 ? segments[0] : null;
} }
}, },
onStateChange: ({ result, loading, error }) => { onStateChange: ({ result, loading, error }) => {
this.setState({ this.setState({
segmentCountLoading: loading, segmentCountLoading: loading,
segmentCount: result, segmentCount: result ? result.count : 0,
unavailableSegmentCount: result ? result.unavailable : 0,
segmentCountError: error, segmentCountError: error,
}); });
}, },
}); });
this.segmentQueryManager.runQuery(`SELECT COUNT(*) as "count" FROM sys.segments`);
// -------------------------
this.supervisorQueryManager = new QueryManager({ this.supervisorQueryManager = new QueryManager({
processQuery: async (query: string) => { processQuery: async () => {
const resp = await axios.get('/druid/indexer/v1/supervisor?full'); const resp = await axios.get('/druid/indexer/v1/supervisor?full');
const data = resp.data; const data = resp.data;
const runningSupervisorCount = data.filter((d: any) => d.spec.suspended === false).length; const runningSupervisorCount = data.filter((d: any) => d.spec.suspended === false).length;
@ -223,12 +221,8 @@ export class HomeView extends React.PureComponent<HomeViewProps, HomeViewState>
}, },
}); });
this.supervisorQueryManager.runQuery('dummy');
// -------------------------
this.taskQueryManager = new QueryManager({ this.taskQueryManager = new QueryManager({
processQuery: async query => { processQuery: async noSqlMode => {
if (noSqlMode) { if (noSqlMode) {
const completeTasksResp = await axios.get('/druid/indexer/v1/completeTasks'); const completeTasksResp = await axios.get('/druid/indexer/v1/completeTasks');
const runningTasksResp = await axios.get('/druid/indexer/v1/runningTasks'); const runningTasksResp = await axios.get('/druid/indexer/v1/runningTasks');
@ -243,7 +237,11 @@ export class HomeView extends React.PureComponent<HomeViewProps, HomeViewState>
}; };
} else { } else {
const taskCountsFromQuery: { status: string; count: number }[] = await queryDruidSql({ const taskCountsFromQuery: { status: string; count: number }[] = await queryDruidSql({
query, query: `SELECT
CASE WHEN "status" = 'RUNNING' THEN "runner_status" ELSE "status" END AS "status",
COUNT (*) AS "count"
FROM sys.tasks
GROUP BY 1`,
}); });
return lookupBy(taskCountsFromQuery, x => x.status, x => x.count); return lookupBy(taskCountsFromQuery, x => x.status, x => x.count);
} }
@ -261,16 +259,8 @@ export class HomeView extends React.PureComponent<HomeViewProps, HomeViewState>
}, },
}); });
this.taskQueryManager.runQuery(`SELECT
CASE WHEN "status" = 'RUNNING' THEN "runner_status" ELSE "status" END AS "status",
COUNT (*) AS "count"
FROM sys.tasks
GROUP BY 1`);
// -------------------------
this.serverQueryManager = new QueryManager({ this.serverQueryManager = new QueryManager({
processQuery: async query => { processQuery: async noSqlMode => {
if (noSqlMode) { if (noSqlMode) {
const serversResp = await axios.get('/druid/coordinator/v1/servers?simple'); const serversResp = await axios.get('/druid/coordinator/v1/servers?simple');
const middleManagerResp = await axios.get('/druid/indexer/v1/workers'); const middleManagerResp = await axios.get('/druid/indexer/v1/workers');
@ -283,7 +273,9 @@ GROUP BY 1`);
const serverCountsFromQuery: { const serverCountsFromQuery: {
server_type: string; server_type: string;
count: number; count: number;
}[] = await queryDruidSql({ query }); }[] = await queryDruidSql({
query: `SELECT server_type, COUNT(*) as "count" FROM sys.servers GROUP BY 1`,
});
return lookupBy(serverCountsFromQuery, x => x.server_type, x => x.count); return lookupBy(serverCountsFromQuery, x => x.server_type, x => x.count);
} }
}, },
@ -301,10 +293,17 @@ GROUP BY 1`);
}); });
}, },
}); });
}
this.serverQueryManager.runQuery( componentDidMount(): void {
`SELECT server_type, COUNT(*) as "count" FROM sys.servers GROUP BY 1`, const { noSqlMode } = this.props;
);
this.statusQueryManager.runQuery(null);
this.datasourceQueryManager.runQuery(noSqlMode);
this.segmentQueryManager.runQuery(noSqlMode);
this.supervisorQueryManager.runQuery(null);
this.taskQueryManager.runQuery(noSqlMode);
this.serverQueryManager.runQuery(noSqlMode);
} }
componentWillUnmount(): void { componentWillUnmount(): void {
@ -362,7 +361,14 @@ GROUP BY 1`);
icon: IconNames.STACKED_CHART, icon: IconNames.STACKED_CHART,
title: 'Segments', title: 'Segments',
loading: state.segmentCountLoading, loading: state.segmentCountLoading,
content: pluralIfNeeded(state.segmentCount, 'segment'), content: (
<>
<p>{pluralIfNeeded(state.segmentCount, 'segment')}</p>
{Boolean(state.unavailableSegmentCount) && (
<p>{pluralIfNeeded(state.unavailableSegmentCount, 'unavailable segment')}</p>
)}
</>
),
error: state.datasourceCountError, error: state.datasourceCountError,
})} })}
{this.renderCard({ {this.renderCard({

View File

@ -153,7 +153,7 @@ exports[`load data view matches snapshot 1`] = `
</div> </div>
</div> </div>
<div <div
className="main" className="main bp3-input"
/> />
<div <div
className="control" className="control"

View File

@ -22,12 +22,12 @@ import ReactTable from 'react-table';
import { TableCell } from '../../../components'; import { TableCell } from '../../../components';
import { caseInsensitiveContains, filterMap } from '../../../utils'; import { caseInsensitiveContains, filterMap } from '../../../utils';
import { DruidFilter, Transform } from '../../../utils/ingestion-spec'; import { DruidFilter } from '../../../utils/ingestion-spec';
import { HeaderAndRows } from '../../../utils/sampler'; import { HeaderAndRows, SampleEntry } from '../../../utils/sampler';
import './filter-table.scss'; import './filter-table.scss';
export interface FilterTableProps extends React.Props<any> { export interface FilterTableProps {
sampleData: HeaderAndRows; sampleData: HeaderAndRows;
columnFilter: string; columnFilter: string;
dimensionFilters: DruidFilter[]; dimensionFilters: DruidFilter[];
@ -82,7 +82,7 @@ export class FilterTable extends React.PureComponent<FilterTableProps> {
headerClassName: columnClassName, headerClassName: columnClassName,
className: columnClassName, className: columnClassName,
id: String(i), id: String(i),
accessor: row => (row.parsed ? row.parsed[columnName] : null), accessor: (row: SampleEntry) => (row.parsed ? row.parsed[columnName] : null),
Cell: row => <TableCell value={row.value} timestamp={timestamp} />, Cell: row => <TableCell value={row.value} timestamp={timestamp} />,
}; };
})} })}

View File

@ -19,7 +19,7 @@
.load-data-view { .load-data-view {
height: 100%; height: 100%;
display: grid; display: grid;
grid-gap: 10px 5px; grid-gap: 15px 5px;
grid-template-columns: 1fr 280px; grid-template-columns: 1fr 280px;
grid-template-rows: 55px 1fr 28px; grid-template-rows: 55px 1fr 28px;
grid-template-areas: grid-template-areas:
@ -29,7 +29,8 @@
&.welcome { &.welcome {
.main { .main {
margin-left: -10px; height: 100%;
padding: 0;
.bp3-card { .bp3-card {
position: relative; position: relative;
@ -37,8 +38,8 @@
vertical-align: top; vertical-align: top;
width: 250px; width: 250px;
height: 140px; height: 140px;
margin-left: 15px; margin-top: 10px;
margin-bottom: 15px; margin-left: 10px;
font-size: 16px; font-size: 16px;
text-align: center; text-align: center;

View File

@ -23,7 +23,7 @@ import { LoadDataView } from './load-data-view';
describe('load data view', () => { describe('load data view', () => {
it('matches snapshot', () => { it('matches snapshot', () => {
const loadDataView = shallow(<LoadDataView goToTask={(taskId: string | null) => {}} />); const loadDataView = shallow(<LoadDataView goToTask={() => {}} />);
expect(loadDataView).toMatchSnapshot(); expect(loadDataView).toMatchSnapshot();
}); });
}); });

View File

@ -25,8 +25,6 @@ import {
Card, Card,
Classes, Classes,
Code, Code,
Dialog,
Elevation,
FormGroup, FormGroup,
H5, H5,
HTMLSelect, HTMLSelect,
@ -221,10 +219,10 @@ const VIEW_TITLE: Record<Step, string> = {
loading: 'Loading', loading: 'Loading',
}; };
export interface LoadDataViewProps extends React.Props<any> { export interface LoadDataViewProps {
initSupervisorId?: string | null; initSupervisorId?: string | null;
initTaskId?: string | null; initTaskId?: string | null;
goToTask: (taskId: string | null, supervisor?: string) => void; goToTask: (taskId: string | undefined, supervisor?: string) => void;
} }
export interface LoadDataViewState { export interface LoadDataViewState {
@ -524,7 +522,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
return ( return (
<> <>
<div className="main"> <div className="main bp3-input">
{this.renderIngestionCard('kafka')} {this.renderIngestionCard('kafka')}
{this.renderIngestionCard('kinesis')} {this.renderIngestionCard('kinesis')}
{this.renderIngestionCard('index:static-s3')} {this.renderIngestionCard('index:static-s3')}
@ -679,7 +677,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
<Button <Button
text="Submit task" text="Submit task"
rightIcon={IconNames.ARROW_RIGHT} rightIcon={IconNames.ARROW_RIGHT}
onClick={() => goToTask(null, 'task')} onClick={() => goToTask(undefined, 'task')}
intent={Intent.PRIMARY} intent={Intent.PRIMARY}
/> />
</FormGroup> </FormGroup>
@ -695,7 +693,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
<Button <Button
text="Submit supervisor" text="Submit supervisor"
rightIcon={IconNames.ARROW_RIGHT} rightIcon={IconNames.ARROW_RIGHT}
onClick={() => goToTask(null, 'supervisor')} onClick={() => goToTask(undefined, 'supervisor')}
intent={Intent.PRIMARY} intent={Intent.PRIMARY}
/> />
</FormGroup> </FormGroup>
@ -703,7 +701,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
<Button <Button
text="Submit task" text="Submit task"
rightIcon={IconNames.ARROW_RIGHT} rightIcon={IconNames.ARROW_RIGHT}
onClick={() => goToTask(null, 'task')} onClick={() => goToTask(undefined, 'task')}
intent={Intent.PRIMARY} intent={Intent.PRIMARY}
/> />
</FormGroup> </FormGroup>
@ -1602,15 +1600,44 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
return; return;
} }
if (sampleResponse.data.length) {
this.setState({
cacheKey: sampleResponse.cacheKey,
filterQueryState: new QueryState({
data: headerAndRowsFromSampleResponse(
sampleResponse,
undefined,
['__time'].concat(parserColumns),
true,
),
}),
});
return;
}
// The filters matched no data
let sampleResponseNoFilter: SampleResponse;
try {
const specNoFilter = deepSet(spec, 'dataSchema.transformSpec.filter', null);
sampleResponseNoFilter = await sampleForFilter(specNoFilter, sampleStrategy, cacheKey);
} catch (e) {
this.setState({
filterQueryState: new QueryState({ error: e.message }),
});
return;
}
const headerAndRowsNoFilter = headerAndRowsFromSampleResponse(
sampleResponseNoFilter,
undefined,
['__time'].concat(parserColumns),
true,
);
this.setState({ this.setState({
cacheKey: sampleResponse.cacheKey, cacheKey: sampleResponseNoFilter.cacheKey,
filterQueryState: new QueryState({ filterQueryState: new QueryState({
data: headerAndRowsFromSampleResponse( data: deepSet(headerAndRowsNoFilter, 'rows', []),
sampleResponse,
undefined,
['__time'].concat(parserColumns),
true,
),
}), }),
}); });
} }
@ -2684,7 +2711,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
}); });
setTimeout(() => { setTimeout(() => {
goToTask(null); goToTask(undefined); // Can we get the supervisor ID here?
}, 1000); }, 1000);
} }
}} }}

View File

@ -27,7 +27,7 @@ import { HeaderAndRows, SampleEntry } from '../../../utils/sampler';
import './parse-data-table.scss'; import './parse-data-table.scss';
export interface ParseDataTableProps extends React.Props<any> { export interface ParseDataTableProps {
sampleData: HeaderAndRows; sampleData: HeaderAndRows;
columnFilter: string; columnFilter: string;
canFlatten: boolean; canFlatten: boolean;

View File

@ -17,4 +17,5 @@
*/ */
.parse-time-table { .parse-time-table {
position: relative;
} }

View File

@ -32,7 +32,7 @@ import { HeaderAndRows, SampleEntry } from '../../../utils/sampler';
import './parse-time-table.scss'; import './parse-time-table.scss';
export interface ParseTimeTableProps extends React.Props<any> { export interface ParseTimeTableProps {
sampleBundle: { sampleBundle: {
headerAndRows: HeaderAndRows; headerAndRows: HeaderAndRows;
timestampSpec: TimestampSpec; timestampSpec: TimestampSpec;

View File

@ -21,12 +21,7 @@ import React from 'react';
import ReactTable from 'react-table'; import ReactTable from 'react-table';
import { TableCell } from '../../../components'; import { TableCell } from '../../../components';
import { import { caseInsensitiveContains, filterMap, sortWithPrefixSuffix } from '../../../utils';
alphanumericCompare,
caseInsensitiveContains,
filterMap,
sortWithPrefixSuffix,
} from '../../../utils';
import { import {
DimensionSpec, DimensionSpec,
DimensionsSpec, DimensionsSpec,
@ -35,13 +30,12 @@ import {
getMetricSpecName, getMetricSpecName,
inflateDimensionSpec, inflateDimensionSpec,
MetricSpec, MetricSpec,
TimestampSpec,
} from '../../../utils/ingestion-spec'; } from '../../../utils/ingestion-spec';
import { HeaderAndRows, SampleEntry } from '../../../utils/sampler'; import { HeaderAndRows, SampleEntry } from '../../../utils/sampler';
import './schema-table.scss'; import './schema-table.scss';
export interface SchemaTableProps extends React.Props<any> { export interface SchemaTableProps {
sampleBundle: { sampleBundle: {
headerAndRows: HeaderAndRows; headerAndRows: HeaderAndRows;
dimensionsSpec: DimensionsSpec; dimensionsSpec: DimensionsSpec;
@ -103,7 +97,7 @@ export class SchemaTable extends React.PureComponent<SchemaTableProps> {
headerClassName: columnClassName, headerClassName: columnClassName,
className: columnClassName, className: columnClassName,
id: String(i), id: String(i),
accessor: row => (row.parsed ? row.parsed[columnName] : null), accessor: (row: SampleEntry) => (row.parsed ? row.parsed[columnName] : null),
Cell: row => <TableCell value={row.value} />, Cell: row => <TableCell value={row.value} />,
}; };
} else { } else {

View File

@ -24,11 +24,11 @@ import { TableCell } from '../../../components';
import { caseInsensitiveContains, filterMap } from '../../../utils'; import { caseInsensitiveContains, filterMap } from '../../../utils';
import { escapeColumnName } from '../../../utils/druid-expression'; import { escapeColumnName } from '../../../utils/druid-expression';
import { Transform } from '../../../utils/ingestion-spec'; import { Transform } from '../../../utils/ingestion-spec';
import { HeaderAndRows } from '../../../utils/sampler'; import { HeaderAndRows, SampleEntry } from '../../../utils/sampler';
import './transform-table.scss'; import './transform-table.scss';
export interface TransformTableProps extends React.Props<any> { export interface TransformTableProps {
sampleData: HeaderAndRows; sampleData: HeaderAndRows;
columnFilter: string; columnFilter: string;
transformedColumnsOnly: boolean; transformedColumnsOnly: boolean;
@ -91,7 +91,7 @@ export class TransformTable extends React.PureComponent<TransformTableProps> {
headerClassName: columnClassName, headerClassName: columnClassName,
className: columnClassName, className: columnClassName,
id: String(i), id: String(i),
accessor: row => (row.parsed ? row.parsed[columnName] : null), accessor: (row: SampleEntry) => (row.parsed ? row.parsed[columnName] : null),
Cell: row => <TableCell value={row.value} timestamp={timestamp} />, Cell: row => <TableCell value={row.value} timestamp={timestamp} />,
}; };
})} })}

View File

@ -7,10 +7,9 @@ exports[`lookups view matches snapshot 1`] = `
<ViewControlBar <ViewControlBar
label="Lookups" label="Lookups"
> >
<Blueprint3.Button <RefreshButton
icon="refresh" localStorageKey="lookups-refresh-rate"
onClick={[Function]} onRefresh={[Function]}
text="Refresh"
/> />
<Blueprint3.Button <Blueprint3.Button
icon="plus" icon="plus"
@ -90,28 +89,28 @@ exports[`lookups view matches snapshot 1`] = `
Array [ Array [
Object { Object {
"Header": "Lookup name", "Header": "Lookup name",
"accessor": [Function], "accessor": "id",
"filterable": true, "filterable": true,
"id": "lookup_name", "id": "lookup_name",
"show": true, "show": true,
}, },
Object { Object {
"Header": "Tier", "Header": "Tier",
"accessor": [Function], "accessor": "tier",
"filterable": true, "filterable": true,
"id": "tier", "id": "tier",
"show": true, "show": true,
}, },
Object { Object {
"Header": "Type", "Header": "Type",
"accessor": [Function], "accessor": "spec.type",
"filterable": true, "filterable": true,
"id": "type", "id": "type",
"show": true, "show": true,
}, },
Object { Object {
"Header": "Version", "Header": "Version",
"accessor": [Function], "accessor": "version",
"filterable": true, "filterable": true,
"id": "version", "id": "version",
"show": true, "show": true,

View File

@ -16,18 +16,17 @@
* limitations under the License. * limitations under the License.
*/ */
import { Button, Icon, Intent, Popover, Position } from '@blueprintjs/core'; import { Button, Intent } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons'; import { IconNames } from '@blueprintjs/icons';
import axios from 'axios'; import axios from 'axios';
import classNames from 'classnames';
import React from 'react'; import React from 'react';
import ReactTable from 'react-table'; import ReactTable from 'react-table';
import { ActionCell, TableColumnSelector, ViewControlBar } from '../../components'; import { ActionCell, RefreshButton, TableColumnSelector, ViewControlBar } from '../../components';
import { AsyncActionDialog, LookupEditDialog } from '../../dialogs/'; import { AsyncActionDialog, LookupEditDialog } from '../../dialogs/';
import { AppToaster } from '../../singletons/toaster'; import { AppToaster } from '../../singletons/toaster';
import { getDruidErrorMessage, LocalStorageKeys, QueryManager } from '../../utils'; import { getDruidErrorMessage, LocalStorageKeys, QueryManager } from '../../utils';
import { BasicAction, basicActionsToMenu } from '../../utils/basic-action'; import { BasicAction } from '../../utils/basic-action';
import { LocalStorageBackedArray } from '../../utils/local-storage-backed-array'; import { LocalStorageBackedArray } from '../../utils/local-storage-backed-array';
import './lookups-view.scss'; import './lookups-view.scss';
@ -36,7 +35,7 @@ const tableColumns: string[] = ['Lookup name', 'Tier', 'Type', 'Version', Action
const DEFAULT_LOOKUP_TIER: string = '__default'; const DEFAULT_LOOKUP_TIER: string = '__default';
export interface LookupsViewProps extends React.Props<any> {} export interface LookupsViewProps {}
export interface LookupsViewState { export interface LookupsViewState {
lookups: {}[] | null; lookups: {}[] | null;
@ -58,7 +57,7 @@ export interface LookupsViewState {
} }
export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsViewState> { export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsViewState> {
private lookupsGetQueryManager: QueryManager<string, { lookupEntries: any[]; tiers: string[] }>; private lookupsQueryManager: QueryManager<null, { lookupEntries: any[]; tiers: string[] }>;
constructor(props: LookupsViewProps, context: any) { constructor(props: LookupsViewProps, context: any) {
super(props, context); super(props, context);
@ -82,11 +81,9 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
LocalStorageKeys.LOOKUP_TABLE_COLUMN_SELECTION, LocalStorageKeys.LOOKUP_TABLE_COLUMN_SELECTION,
), ),
}; };
}
componentDidMount(): void { this.lookupsQueryManager = new QueryManager({
this.lookupsGetQueryManager = new QueryManager({ processQuery: async () => {
processQuery: async (query: string) => {
const tiersResp = await axios.get('/druid/coordinator/v1/lookups/config?discover=true'); const tiersResp = await axios.get('/druid/coordinator/v1/lookups/config?discover=true');
const tiers = const tiers =
tiersResp.data && tiersResp.data.length > 0 ? tiersResp.data : [DEFAULT_LOOKUP_TIER]; tiersResp.data && tiersResp.data.length > 0 ? tiersResp.data : [DEFAULT_LOOKUP_TIER];
@ -121,18 +118,20 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
}); });
}, },
}); });
}
this.lookupsGetQueryManager.runQuery('dummy'); componentDidMount(): void {
this.lookupsQueryManager.runQuery(null);
} }
componentWillUnmount(): void { componentWillUnmount(): void {
this.lookupsGetQueryManager.terminate(); this.lookupsQueryManager.terminate();
} }
private async initializeLookup() { private async initializeLookup() {
try { try {
await axios.post(`/druid/coordinator/v1/lookups/config`, {}); await axios.post(`/druid/coordinator/v1/lookups/config`, {});
this.lookupsGetQueryManager.rerunLastQuery(); this.lookupsQueryManager.rerunLastQuery();
} catch (e) { } catch (e) {
AppToaster.show({ AppToaster.show({
icon: IconNames.ERROR, icon: IconNames.ERROR,
@ -208,7 +207,7 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
this.setState({ this.setState({
lookupEditDialogOpen: false, lookupEditDialogOpen: false,
}); });
this.lookupsGetQueryManager.rerunLastQuery(); this.lookupsQueryManager.rerunLastQuery();
} catch (e) { } catch (e) {
AppToaster.show({ AppToaster.show({
icon: IconNames.ERROR, icon: IconNames.ERROR,
@ -254,7 +253,7 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
intent={Intent.DANGER} intent={Intent.DANGER}
onClose={success => { onClose={success => {
this.setState({ deleteLookupTier: null, deleteLookupName: null }); this.setState({ deleteLookupTier: null, deleteLookupName: null });
if (success) this.lookupsGetQueryManager.rerunLastQuery(); if (success) this.lookupsQueryManager.rerunLastQuery();
}} }}
> >
<p>{`Are you sure you want to delete the lookup '${deleteLookupName}'?`}</p> <p>{`Are you sure you want to delete the lookup '${deleteLookupName}'?`}</p>
@ -296,28 +295,28 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
{ {
Header: 'Lookup name', Header: 'Lookup name',
id: 'lookup_name', id: 'lookup_name',
accessor: (row: any) => row.id, accessor: 'id',
filterable: true, filterable: true,
show: hiddenColumns.exists('Lookup name'), show: hiddenColumns.exists('Lookup name'),
}, },
{ {
Header: 'Tier', Header: 'Tier',
id: 'tier', id: 'tier',
accessor: (row: any) => row.tier, accessor: 'tier',
filterable: true, filterable: true,
show: hiddenColumns.exists('Tier'), show: hiddenColumns.exists('Tier'),
}, },
{ {
Header: 'Type', Header: 'Type',
id: 'type', id: 'type',
accessor: (row: any) => row.spec.type, accessor: 'spec.type',
filterable: true, filterable: true,
show: hiddenColumns.exists('Type'), show: hiddenColumns.exists('Type'),
}, },
{ {
Header: 'Version', Header: 'Version',
id: 'version', id: 'version',
accessor: (row: any) => row.version, accessor: 'version',
filterable: true, filterable: true,
show: hiddenColumns.exists('Version'), show: hiddenColumns.exists('Version'),
}, },
@ -325,7 +324,7 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
Header: ActionCell.COLUMN_LABEL, Header: ActionCell.COLUMN_LABEL,
id: ActionCell.COLUMN_ID, id: ActionCell.COLUMN_ID,
width: ActionCell.COLUMN_WIDTH, width: ActionCell.COLUMN_WIDTH,
accessor: row => ({ id: row.id, tier: row.tier }), accessor: (row: any) => ({ id: row.id, tier: row.tier }),
filterable: false, filterable: false,
Cell: (row: any) => { Cell: (row: any) => {
const lookupId = row.value.id; const lookupId = row.value.id;
@ -375,10 +374,9 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
return ( return (
<div className="lookups-view app-view"> <div className="lookups-view app-view">
<ViewControlBar label="Lookups"> <ViewControlBar label="Lookups">
<Button <RefreshButton
icon={IconNames.REFRESH} onRefresh={() => this.lookupsQueryManager.rerunLastQuery()}
text="Refresh" localStorageKey={LocalStorageKeys.LOOKUPS_REFRESH_RATE}
onClick={() => this.lookupsGetQueryManager.rerunLastQuery()}
/> />
{!lookupsError && ( {!lookupsError && (
<Button <Button

View File

@ -26,7 +26,7 @@ import { ColumnMetadata } from '../../../utils/column-metadata';
import './column-tree.scss'; import './column-tree.scss';
export interface ColumnTreeProps extends React.Props<any> { export interface ColumnTreeProps {
columnMetadataLoading: boolean; columnMetadataLoading: boolean;
columnMetadata: ColumnMetadata[] | null; columnMetadata: ColumnMetadata[] | null;
onQueryStringChange: (queryString: string) => void; onQueryStringChange: (queryString: string) => void;
@ -150,11 +150,7 @@ export class ColumnTree extends React.PureComponent<ColumnTreeProps, ColumnTreeS
); );
} }
private handleNodeClick = ( private handleNodeClick = (nodeData: ITreeNode, nodePath: number[]) => {
nodeData: ITreeNode,
nodePath: number[],
e: React.MouseEvent<HTMLElement>,
) => {
const { onQueryStringChange } = this.props; const { onQueryStringChange } = this.props;
const { columnTree, selectedTreeIndex } = this.state; const { columnTree, selectedTreeIndex } = this.state;
if (!columnTree) return; if (!columnTree) return;

View File

@ -44,7 +44,7 @@ export interface QueryExtraInfoData {
wrappedLimit?: number; wrappedLimit?: number;
} }
export interface QueryExtraInfoProps extends React.Props<any> { export interface QueryExtraInfoProps {
queryExtraInfo: QueryExtraInfoData; queryExtraInfo: QueryExtraInfoData;
onDownload: (filename: string, format: string) => void; onDownload: (filename: string, format: string) => void;
} }

View File

@ -33,7 +33,7 @@ import './query-input.scss';
const langTools = ace.acequire('ace/ext/language_tools'); const langTools = ace.acequire('ace/ext/language_tools');
export interface QueryInputProps extends React.Props<any> { export interface QueryInputProps {
queryString: string; queryString: string;
onQueryStringChange: (newQueryString: string) => void; onQueryStringChange: (newQueryString: string) => void;
runeMode: boolean; runeMode: boolean;
@ -71,7 +71,7 @@ export class QueryInput extends React.PureComponent<QueryInputProps, QueryInputS
); );
langTools.addCompleter({ langTools.addCompleter({
getCompletions: (editor: any, session: any, pos: any, prefix: any, callback: any) => { getCompletions: (_editor: any, _session: any, _pos: any, _prefix: any, callback: any) => {
callback(null, completions); callback(null, completions);
}, },
}); });
@ -103,7 +103,7 @@ export class QueryInput extends React.PureComponent<QueryInputProps, QueryInputS
); );
const keywordCompleter = { const keywordCompleter = {
getCompletions: (editor: any, session: any, pos: any, prefix: any, callback: any) => { getCompletions: (_editor: any, _session: any, _pos: any, _prefix: any, callback: any) => {
return callback(null, keywordList); return callback(null, keywordList);
}, },
}; };
@ -137,7 +137,7 @@ export class QueryInput extends React.PureComponent<QueryInputProps, QueryInputS
}); });
langTools.addCompleter({ langTools.addCompleter({
getCompletions: (editor: any, session: any, pos: any, prefix: any, callback: any) => { getCompletions: (_editor: any, _session: any, _pos: any, _prefix: any, callback: any) => {
callback(null, functionList); callback(null, functionList);
}, },
getDocTooltip: (item: any) => { getDocTooltip: (item: any) => {

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