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(
"/",
"/coordinator-console/*",
"/assets/*",
"/public/*",
"/old-console/*",
"/pages/*",

View File

@ -1,6 +1,6 @@
{
"name": "web-console",
"version": "0.15.0",
"version": "0.16.0",
"description": "A web console for Apache Druid",
"author": "Imply Data Inc.",
"license": "Apache-2.0",
@ -37,7 +37,7 @@
"compile": "./script/build",
"pretest": "./script/build",
"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",
"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)'",

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,7 +16,7 @@
* 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 copy from 'copy-to-clipboard';
import React from 'react';
@ -27,7 +27,7 @@ import { downloadFile } from '../../utils';
import './show-json.scss';
export interface ShowJsonProps extends React.Props<any> {
export interface ShowJsonProps {
endpoint: string;
transform?: (x: any) => any;
downloadFilename?: string;

View File

@ -16,7 +16,7 @@
* 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 copy from 'copy-to-clipboard';
import React from 'react';
@ -35,7 +35,7 @@ function removeFirstPartialLine(log: string): string {
return lines.join('\n');
}
export interface ShowLogProps extends React.Props<any> {
export interface ShowLogProps {
endpoint: string;
downloadFilename?: string;
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 { 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', () => {
const lookupEditDialog = (
<LookupEditDialog
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 suggestibleInput = (
<SuggestibleInput onValueChange={() => {}} suggestions={['a', 'b', 'c']} />
);
const { container } = render(lookupEditDialog, { container: document.body });
const { container } = render(suggestibleInput);
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.
*/
import { Intent } from '@blueprintjs/core';
import { IconNames } from '@blueprintjs/icons';
import copy from 'copy-to-clipboard';
import React from 'react';
import { AppToaster } from '../../singletons/toaster';
import { ActionIcon } from '../action-icon/action-icon';
import './table-cell.scss';
export interface NullTableCellProps extends React.Props<any> {
export interface NullTableCellProps {
value?: any;
timestamp?: boolean;
unparseable?: boolean;

View File

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

View File

@ -16,7 +16,7 @@
* 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 React from 'react';
@ -24,7 +24,7 @@ import { MenuCheckbox } from '../menu-checkbox/menu-checkbox';
import './table-column-selector.scss';
interface TableColumnSelectorProps extends React.Props<any> {
interface TableColumnSelectorProps {
columns: string[];
onChange: (column: string) => void;
tableColumnsHidden: string[];

View File

@ -18,5 +18,4 @@
.timed-button {
padding: 10px 10px 5px 10px;
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -16,7 +16,7 @@ exports[`coordinator dynamic config matches snapshot 1`] = `
tabindex="0"
>
<div
class="bp3-dialog snitch-dialog coordinator-dynamic-config"
class="bp3-dialog snitch-dialog coordinator-dynamic-config-dialog"
>
<div
class="bp3-dialog-header"
@ -76,16 +76,6 @@ exports[`coordinator dynamic config matches snapshot 1`] = `
<div
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
class="bp3-button bp3-intent-primary"
type="button"

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -18,10 +18,10 @@
export * from './about-dialog/about-dialog';
export * from './async-action-dialog/async-action-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 './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 './retention-dialog/retention-dialog';
export * from './snitch-dialog/snitch-dialog';

View File

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

View File

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

View File

@ -26,9 +26,9 @@ import { AppToaster } from '../../singletons/toaster';
import { getDruidErrorMessage, QueryManager } from '../../utils';
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;
}
@ -51,22 +51,22 @@ export class OverlordDynamicConfigDialog extends React.PureComponent<
allJSONValid: true,
historyRecords: [],
};
}
componentDidMount() {
this.getConfig();
this.historyQueryManager = new QueryManager({
processQuery: async query => {
processQuery: async () => {
const historyResp = await axios(`/druid/indexer/v1/worker/history?count=100`);
return historyResp.data;
},
onStateChange: ({ result, loading, error }) => {
onStateChange: ({ result }) => {
this.setState({
historyRecords: result,
});
},
});
}
componentDidMount() {
this.getConfig();
this.historyQueryManager.runQuery(`dummy`);
}
@ -120,7 +120,7 @@ export class OverlordDynamicConfigDialog extends React.PureComponent<
return (
<SnitchDialog
className="overlord-dynamic-config"
className="overlord-dynamic-config-dialog"
isOpen
onSave={this.saveConfig}
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}
/>
);
const { container } = render(queryPlanDialog, { container: document.body });
expect(container.firstChild).toMatchSnapshot();
render(queryPlanDialog);
expect(document.body.lastChild).toMatchSnapshot();
});
});

View File

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

View File

@ -116,16 +116,6 @@ exports[`retention dialog matches snapshot 1`] = `
<div
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
class="bp3-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 { render } from 'react-testing-library';
import { RetentionDialog } from './retention-dialog';
import { reorderArray, RetentionDialog } from './retention-dialog';
describe('retention dialog', () => {
it('matches snapshot', () => {
@ -33,7 +33,42 @@ describe('retention dialog', () => {
onSave={() => null}
/>
);
const { container } = render(retentionDialog, { container: document.body });
expect(container.firstChild).toMatchSnapshot();
render(retentionDialog);
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 React from 'react';
import { Rule, RuleEditor } from '../../components';
import { RuleEditor } from '../../components';
import { QueryManager } from '../../utils';
import { SnitchDialog } from '../snitch-dialog/snitch-dialog';
@ -37,7 +37,7 @@ export function reorderArray<T>(items: T[], oldIndex: number, newIndex: number):
return newItems;
}
export interface RetentionDialogProps extends React.Props<any> {
export interface RetentionDialogProps {
datasource: string;
rules: any[];
tiers: string[];
@ -64,23 +64,23 @@ export class RetentionDialog extends React.PureComponent<
currentRules: props.rules,
historyRecords: [],
};
}
componentDidMount() {
const { datasource } = this.props;
this.historyQueryManager = new QueryManager({
processQuery: async query => {
processQuery: async datasource => {
const historyResp = await axios(`/druid/coordinator/v1/rules/${datasource}/history`);
return historyResp.data;
},
onStateChange: ({ result, loading, error }) => {
onStateChange: ({ result }) => {
this.setState({
historyRecords: result,
});
},
});
}
this.historyQueryManager.runQuery(`dummy`);
componentDidMount() {
const { datasource } = this.props;
this.historyQueryManager.runQuery(datasource);
}
private save = (comment: string) => {
@ -106,7 +106,7 @@ export class RetentionDialog extends React.PureComponent<
onDeleteRule = (index: number) => {
const { currentRules } = this.state;
const newRules = (currentRules || []).filter((r, i) => i !== index);
const newRules = (currentRules || []).filter((_r, i) => i !== index);
this.setState({
currentRules: newRules,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -30,7 +30,7 @@ describe('table action dialog', () => {
isOpen
/>
);
const { container } = render(tableActionDialog, { container: document.body });
expect(container.firstChild).toMatchSnapshot();
render(tableActionDialog);
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}>
<div className={Classes.DIALOG_BODY}>
<div className="side-bar">
{sideButtonMetadata.map((d: SideButtonMetaData) => (
{sideButtonMetadata.map((d, i) => (
<Button
className="tab-button"
icon={<Icon icon={d.icon} iconSize={20} />}
key={d.text}
key={i}
text={d.text}
intent={d.active ? Intent.PRIMARY : Intent.NONE}
minimal={!d.active}

View File

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

View File

@ -16,7 +16,7 @@
* 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 FileSaver from 'file-saver';
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}`;
}
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 {
@ -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[] {
return (xs.map(f) as any).filter(Boolean);
}

View File

@ -17,7 +17,6 @@
*/
import { Code } from '@blueprintjs/core';
import { number } from 'prop-types';
import React from 'react';
import { Field } from '../components/auto-form/auto-form';
@ -184,11 +183,12 @@ export interface ParseSpec {
export function hasParallelAbility(spec: IngestionSpec): boolean {
const specType = getSpecType(spec);
return spec.type === 'index' || spec.type === 'index_parallel';
return specType === 'index' || specType === 'index_parallel';
}
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';
@ -1482,6 +1482,45 @@ const TUNING_CONFIG_FORM_FIELDS: Field<TuningConfig>[] = [
placeholder: 'Default: 1/6 of max JVM memory',
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',
type: 'duration',
@ -1513,10 +1552,6 @@ const TUNING_CONFIG_FORM_FIELDS: Field<TuningConfig>[] = [
</>
),
},
{
name: 'forceExtendableShardSpecs',
type: 'boolean',
},
{
name: 'pushTimeout',
type: 'number',

View File

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

View File

@ -32,6 +32,7 @@ export const LocalStorageKeys = {
SEGMENTS_REFRESH_RATE: 'segments-refresh-rate' as 'segments-refresh-rate',
SERVERS_REFRESH_RATE: 'servers-refresh-rate' as 'servers-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];

View File

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

View File

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

View File

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

View File

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

View File

@ -19,10 +19,12 @@
import { Card, H5, Icon } from '@blueprintjs/core';
import { IconName, IconNames } from '@blueprintjs/icons';
import axios from 'axios';
import { sum } from 'd3-array';
import React from 'react';
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';
@ -35,7 +37,7 @@ export interface CardOptions {
error?: string | null;
}
export interface HomeViewProps extends React.Props<any> {
export interface HomeViewProps {
noSqlMode: boolean;
}
@ -50,6 +52,7 @@ export interface HomeViewState {
segmentCountLoading: boolean;
segmentCount: number;
unavailableSegmentCount: number;
segmentCountError: string | null;
supervisorCountLoading: boolean;
@ -77,12 +80,12 @@ export interface HomeViewState {
}
export class HomeView extends React.PureComponent<HomeViewProps, HomeViewState> {
private statusQueryManager: QueryManager<string, any>;
private datasourceQueryManager: QueryManager<string, any>;
private segmentQueryManager: QueryManager<string, any>;
private supervisorQueryManager: QueryManager<string, any>;
private taskQueryManager: QueryManager<string, any>;
private serverQueryManager: QueryManager<string, any>;
private statusQueryManager: QueryManager<null, any>;
private datasourceQueryManager: QueryManager<boolean, any>;
private segmentQueryManager: QueryManager<boolean, any>;
private supervisorQueryManager: QueryManager<null, any>;
private taskQueryManager: QueryManager<boolean, any>;
private serverQueryManager: QueryManager<boolean, any>;
constructor(props: HomeViewProps, context: any) {
super(props, context);
@ -97,6 +100,7 @@ export class HomeView extends React.PureComponent<HomeViewProps, HomeViewState>
segmentCountLoading: false,
segmentCount: 0,
unavailableSegmentCount: 0,
segmentCountError: null,
supervisorCountLoading: false,
@ -122,13 +126,9 @@ export class HomeView extends React.PureComponent<HomeViewProps, HomeViewState>
peonCount: 0,
serverCountError: null,
};
}
componentDidMount(): void {
const { noSqlMode } = this.props;
this.statusQueryManager = new QueryManager({
processQuery: async query => {
processQuery: async () => {
const statusResp = await axios.get('/status');
return statusResp.data;
},
@ -141,15 +141,13 @@ export class HomeView extends React.PureComponent<HomeViewProps, HomeViewState>
},
});
this.statusQueryManager.runQuery(`dummy`);
// -------------------------
this.datasourceQueryManager = new QueryManager({
processQuery: async query => {
processQuery: async noSqlMode => {
let datasources: string[];
if (!noSqlMode) {
datasources = await queryDruidSql({ query });
datasources = await queryDruidSql({
query: `SELECT datasource FROM sys.segments GROUP BY 1`,
});
} else {
const datasourcesResp = await axios.get('/druid/coordinator/v1/datasources');
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({
processQuery: async query => {
processQuery: async noSqlMode => {
if (noSqlMode) {
const loadstatusResp = await axios.get('/druid/coordinator/v1/loadstatus?simple');
const loadstatus = loadstatusResp.data;
const unavailableSegmentNum = Object.keys(loadstatus).reduce((sum, key) => {
return sum + loadstatus[key];
}, 0);
const unavailableSegmentNum = sum(Object.keys(loadstatus), key => loadstatus[key]);
const datasourcesMetaResp = await axios.get('/druid/coordinator/v1/datasources?simple');
const datasourcesMeta = datasourcesMetaResp.data;
const availableSegmentNum = datasourcesMeta.reduce((sum: number, curr: any) => {
return sum + curr.properties.segments.count;
}, 0);
const availableSegmentNum = sum(datasourcesMeta, (curr: any) =>
deepGet(curr, 'properties.segments.count'),
);
return availableSegmentNum + unavailableSegmentNum;
return {
count: availableSegmentNum + unavailableSegmentNum,
unavailable: unavailableSegmentNum,
};
} else {
const segments = await queryDruidSql({ query });
return getHeadProp(segments, 'count') || 0;
const segments = await queryDruidSql({
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 }) => {
this.setState({
segmentCountLoading: loading,
segmentCount: result,
segmentCount: result ? result.count : 0,
unavailableSegmentCount: result ? result.unavailable : 0,
segmentCountError: error,
});
},
});
this.segmentQueryManager.runQuery(`SELECT COUNT(*) as "count" FROM sys.segments`);
// -------------------------
this.supervisorQueryManager = new QueryManager({
processQuery: async (query: string) => {
processQuery: async () => {
const resp = await axios.get('/druid/indexer/v1/supervisor?full');
const data = resp.data;
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({
processQuery: async query => {
processQuery: async noSqlMode => {
if (noSqlMode) {
const completeTasksResp = await axios.get('/druid/indexer/v1/completeTasks');
const runningTasksResp = await axios.get('/druid/indexer/v1/runningTasks');
@ -243,7 +237,11 @@ export class HomeView extends React.PureComponent<HomeViewProps, HomeViewState>
};
} else {
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);
}
@ -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({
processQuery: async query => {
processQuery: async noSqlMode => {
if (noSqlMode) {
const serversResp = await axios.get('/druid/coordinator/v1/servers?simple');
const middleManagerResp = await axios.get('/druid/indexer/v1/workers');
@ -283,7 +273,9 @@ GROUP BY 1`);
const serverCountsFromQuery: {
server_type: string;
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);
}
},
@ -301,10 +293,17 @@ GROUP BY 1`);
});
},
});
}
this.serverQueryManager.runQuery(
`SELECT server_type, COUNT(*) as "count" FROM sys.servers GROUP BY 1`,
);
componentDidMount(): void {
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 {
@ -362,7 +361,14 @@ GROUP BY 1`);
icon: IconNames.STACKED_CHART,
title: 'Segments',
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,
})}
{this.renderCard({

View File

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

View File

@ -22,12 +22,12 @@ import ReactTable from 'react-table';
import { TableCell } from '../../../components';
import { caseInsensitiveContains, filterMap } from '../../../utils';
import { DruidFilter, Transform } from '../../../utils/ingestion-spec';
import { HeaderAndRows } from '../../../utils/sampler';
import { DruidFilter } from '../../../utils/ingestion-spec';
import { HeaderAndRows, SampleEntry } from '../../../utils/sampler';
import './filter-table.scss';
export interface FilterTableProps extends React.Props<any> {
export interface FilterTableProps {
sampleData: HeaderAndRows;
columnFilter: string;
dimensionFilters: DruidFilter[];
@ -82,7 +82,7 @@ export class FilterTable extends React.PureComponent<FilterTableProps> {
headerClassName: columnClassName,
className: columnClassName,
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} />,
};
})}

View File

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

View File

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

View File

@ -25,8 +25,6 @@ import {
Card,
Classes,
Code,
Dialog,
Elevation,
FormGroup,
H5,
HTMLSelect,
@ -221,10 +219,10 @@ const VIEW_TITLE: Record<Step, string> = {
loading: 'Loading',
};
export interface LoadDataViewProps extends React.Props<any> {
export interface LoadDataViewProps {
initSupervisorId?: string | null;
initTaskId?: string | null;
goToTask: (taskId: string | null, supervisor?: string) => void;
goToTask: (taskId: string | undefined, supervisor?: string) => void;
}
export interface LoadDataViewState {
@ -524,7 +522,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
return (
<>
<div className="main">
<div className="main bp3-input">
{this.renderIngestionCard('kafka')}
{this.renderIngestionCard('kinesis')}
{this.renderIngestionCard('index:static-s3')}
@ -679,7 +677,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
<Button
text="Submit task"
rightIcon={IconNames.ARROW_RIGHT}
onClick={() => goToTask(null, 'task')}
onClick={() => goToTask(undefined, 'task')}
intent={Intent.PRIMARY}
/>
</FormGroup>
@ -695,7 +693,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
<Button
text="Submit supervisor"
rightIcon={IconNames.ARROW_RIGHT}
onClick={() => goToTask(null, 'supervisor')}
onClick={() => goToTask(undefined, 'supervisor')}
intent={Intent.PRIMARY}
/>
</FormGroup>
@ -703,7 +701,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
<Button
text="Submit task"
rightIcon={IconNames.ARROW_RIGHT}
onClick={() => goToTask(null, 'task')}
onClick={() => goToTask(undefined, 'task')}
intent={Intent.PRIMARY}
/>
</FormGroup>
@ -1602,6 +1600,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
return;
}
if (sampleResponse.data.length) {
this.setState({
cacheKey: sampleResponse.cacheKey,
filterQueryState: new QueryState({
@ -1613,6 +1612,34 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
),
}),
});
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({
cacheKey: sampleResponseNoFilter.cacheKey,
filterQueryState: new QueryState({
data: deepSet(headerAndRowsNoFilter, 'rows', []),
}),
});
}
private getMemoizedDimensionFiltersFromSpec = memoize(spec => {
@ -2684,7 +2711,7 @@ export class LoadDataView extends React.PureComponent<LoadDataViewProps, LoadDat
});
setTimeout(() => {
goToTask(null);
goToTask(undefined); // Can we get the supervisor ID here?
}, 1000);
}
}}

View File

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

View File

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

View File

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

View File

@ -21,12 +21,7 @@ import React from 'react';
import ReactTable from 'react-table';
import { TableCell } from '../../../components';
import {
alphanumericCompare,
caseInsensitiveContains,
filterMap,
sortWithPrefixSuffix,
} from '../../../utils';
import { caseInsensitiveContains, filterMap, sortWithPrefixSuffix } from '../../../utils';
import {
DimensionSpec,
DimensionsSpec,
@ -35,13 +30,12 @@ import {
getMetricSpecName,
inflateDimensionSpec,
MetricSpec,
TimestampSpec,
} from '../../../utils/ingestion-spec';
import { HeaderAndRows, SampleEntry } from '../../../utils/sampler';
import './schema-table.scss';
export interface SchemaTableProps extends React.Props<any> {
export interface SchemaTableProps {
sampleBundle: {
headerAndRows: HeaderAndRows;
dimensionsSpec: DimensionsSpec;
@ -103,7 +97,7 @@ export class SchemaTable extends React.PureComponent<SchemaTableProps> {
headerClassName: columnClassName,
className: columnClassName,
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} />,
};
} else {

View File

@ -24,11 +24,11 @@ import { TableCell } from '../../../components';
import { caseInsensitiveContains, filterMap } from '../../../utils';
import { escapeColumnName } from '../../../utils/druid-expression';
import { Transform } from '../../../utils/ingestion-spec';
import { HeaderAndRows } from '../../../utils/sampler';
import { HeaderAndRows, SampleEntry } from '../../../utils/sampler';
import './transform-table.scss';
export interface TransformTableProps extends React.Props<any> {
export interface TransformTableProps {
sampleData: HeaderAndRows;
columnFilter: string;
transformedColumnsOnly: boolean;
@ -91,7 +91,7 @@ export class TransformTable extends React.PureComponent<TransformTableProps> {
headerClassName: columnClassName,
className: columnClassName,
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} />,
};
})}

View File

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

View File

@ -16,18 +16,17 @@
* 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 axios from 'axios';
import classNames from 'classnames';
import React from 'react';
import ReactTable from 'react-table';
import { ActionCell, TableColumnSelector, ViewControlBar } from '../../components';
import { ActionCell, RefreshButton, TableColumnSelector, ViewControlBar } from '../../components';
import { AsyncActionDialog, LookupEditDialog } from '../../dialogs/';
import { AppToaster } from '../../singletons/toaster';
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 './lookups-view.scss';
@ -36,7 +35,7 @@ const tableColumns: string[] = ['Lookup name', 'Tier', 'Type', 'Version', Action
const DEFAULT_LOOKUP_TIER: string = '__default';
export interface LookupsViewProps extends React.Props<any> {}
export interface LookupsViewProps {}
export interface LookupsViewState {
lookups: {}[] | null;
@ -58,7 +57,7 @@ export interface 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) {
super(props, context);
@ -82,11 +81,9 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
LocalStorageKeys.LOOKUP_TABLE_COLUMN_SELECTION,
),
};
}
componentDidMount(): void {
this.lookupsGetQueryManager = new QueryManager({
processQuery: async (query: string) => {
this.lookupsQueryManager = new QueryManager({
processQuery: async () => {
const tiersResp = await axios.get('/druid/coordinator/v1/lookups/config?discover=true');
const tiers =
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 {
this.lookupsGetQueryManager.terminate();
this.lookupsQueryManager.terminate();
}
private async initializeLookup() {
try {
await axios.post(`/druid/coordinator/v1/lookups/config`, {});
this.lookupsGetQueryManager.rerunLastQuery();
this.lookupsQueryManager.rerunLastQuery();
} catch (e) {
AppToaster.show({
icon: IconNames.ERROR,
@ -208,7 +207,7 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
this.setState({
lookupEditDialogOpen: false,
});
this.lookupsGetQueryManager.rerunLastQuery();
this.lookupsQueryManager.rerunLastQuery();
} catch (e) {
AppToaster.show({
icon: IconNames.ERROR,
@ -254,7 +253,7 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
intent={Intent.DANGER}
onClose={success => {
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>
@ -296,28 +295,28 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
{
Header: 'Lookup name',
id: 'lookup_name',
accessor: (row: any) => row.id,
accessor: 'id',
filterable: true,
show: hiddenColumns.exists('Lookup name'),
},
{
Header: 'Tier',
id: 'tier',
accessor: (row: any) => row.tier,
accessor: 'tier',
filterable: true,
show: hiddenColumns.exists('Tier'),
},
{
Header: 'Type',
id: 'type',
accessor: (row: any) => row.spec.type,
accessor: 'spec.type',
filterable: true,
show: hiddenColumns.exists('Type'),
},
{
Header: 'Version',
id: 'version',
accessor: (row: any) => row.version,
accessor: 'version',
filterable: true,
show: hiddenColumns.exists('Version'),
},
@ -325,7 +324,7 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
Header: ActionCell.COLUMN_LABEL,
id: ActionCell.COLUMN_ID,
width: ActionCell.COLUMN_WIDTH,
accessor: row => ({ id: row.id, tier: row.tier }),
accessor: (row: any) => ({ id: row.id, tier: row.tier }),
filterable: false,
Cell: (row: any) => {
const lookupId = row.value.id;
@ -375,10 +374,9 @@ export class LookupsView extends React.PureComponent<LookupsViewProps, LookupsVi
return (
<div className="lookups-view app-view">
<ViewControlBar label="Lookups">
<Button
icon={IconNames.REFRESH}
text="Refresh"
onClick={() => this.lookupsGetQueryManager.rerunLastQuery()}
<RefreshButton
onRefresh={() => this.lookupsQueryManager.rerunLastQuery()}
localStorageKey={LocalStorageKeys.LOOKUPS_REFRESH_RATE}
/>
{!lookupsError && (
<Button

View File

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

View File

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

View File

@ -33,7 +33,7 @@ import './query-input.scss';
const langTools = ace.acequire('ace/ext/language_tools');
export interface QueryInputProps extends React.Props<any> {
export interface QueryInputProps {
queryString: string;
onQueryStringChange: (newQueryString: string) => void;
runeMode: boolean;
@ -71,7 +71,7 @@ export class QueryInput extends React.PureComponent<QueryInputProps, QueryInputS
);
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);
},
});
@ -103,7 +103,7 @@ export class QueryInput extends React.PureComponent<QueryInputProps, QueryInputS
);
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);
},
};
@ -137,7 +137,7 @@ export class QueryInput extends React.PureComponent<QueryInputProps, QueryInputS
});
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);
},
getDocTooltip: (item: any) => {

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