Add lookups view to allow adding/editing/deleting of druid lookups (#7259)

* Add lookups view to allow adding/editing/deleting of druid lookups

* Remove unused bp3 class

* Make lookup editor dialog wider
This commit is contained in:
Qi Shu 2019-03-16 01:46:43 -07:00 committed by Clint Wylie
parent 5406aaa49d
commit 1b6b40e511
8 changed files with 554 additions and 1 deletions

View File

@ -49,6 +49,8 @@ export const IconNames = {
CARET_DOWN: "caret-down" as "caret-down",
ARROW_UP: "arrow-up" as "arrow-up",
ARROW_DOWN: "arrow-down" as "arrow-down",
PROPERTIES: "properties" as "properties",
BUILD: "build" as "build"
};
export type IconNames = typeof IconNames[keyof typeof IconNames];

View File

@ -31,7 +31,7 @@ import {
LEGACY_OVERLORD_CONSOLE
} from '../variables';
export type HeaderActiveTab = null | 'datasources' | 'segments' | 'tasks' | 'servers' | 'sql';
export type HeaderActiveTab = null | 'datasources' | 'segments' | 'tasks' | 'servers' | 'sql' | 'lookups';
export interface HeaderBarProps extends React.Props<any> {
active: HeaderActiveTab;
@ -100,6 +100,7 @@ export class HeaderBar extends React.Component<HeaderBarProps, HeaderBarState> {
const configMenu = <Menu>
<MenuItem iconName={IconNames.COG} text="Coordinator dynamic config" onClick={() => this.setState({ coordinatorDynamicConfigDialogOpen: true })}/>
<MenuItem iconName={IconNames.PROPERTIES} className={classNames(Classes.MINIMAL, { 'pt-active': active === 'lookups' })} text="Lookups" href="#lookups"/>
</Menu>;
return <Navbar className="header-bar">

View File

@ -31,6 +31,7 @@ import { SegmentsView } from './views/segments-view';
import { ServersView } from './views/servers-view';
import { TasksView } from './views/tasks-view';
import { SqlView } from './views/sql-view';
import { LookupsView } from "./views/lookups-view";
import "./console-application.scss";
export interface ConsoleApplicationProps extends React.Props<any> {
@ -156,6 +157,9 @@ export class ConsoleApplication extends React.Component<ConsoleApplicationProps,
<Route path="/sql" component={() => {
return wrapInViewContainer('sql', <SqlView initSql={this.initSql}/>);
}} />
<Route path="/lookups" component={() => {
return wrapInViewContainer('lookups', <LookupsView />);
}} />
<Route component={() => {
return wrapInViewContainer(null, <HomeView/>)
}} />

View File

@ -0,0 +1,41 @@
/*
* 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.
*/
.lookup-edit-dialog {
top: 10vh;
width: 600px;
.ace_editor{
margin: 0px 20px 10px;
}
.lookup-label {
padding: 0 20px;
margin-top: 5px;
margin-bottom: 5px;
}
.ace_scroller {
background-color: #232C35;
}
.ace_gutter-layer {
background-color: #27313c;
}
}

View File

@ -0,0 +1,154 @@
/*
* 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 * as React from "react";
import {Button, Classes, Dialog, Intent, InputGroup } from "@blueprintjs/core";
import "./lookup-edit-dialog.scss"
import {validJson} from "../utils";
import AceEditor from "react-ace";
import {FormGroup} from "../components/filler";
export interface LookupEditDialogProps extends React.Props<any> {
isOpen: boolean,
onClose: () => void,
onSubmit: () => void,
onChange: (field: string, value: string) => void
lookupName: string,
lookupTier: string,
lookupVersion: string,
lookupSpec: string,
isEdit: boolean,
allLookupTiers: string[]
}
export interface LookupEditDialogState {
}
export class LookupEditDialog extends React.Component<LookupEditDialogProps, LookupEditDialogState> {
constructor(props: LookupEditDialogProps) {
super(props);
this.state = {
}
}
private addISOVersion = () => {
const {onChange} = this.props;
const currentDate = new Date();
const ISOString = currentDate.toISOString();
onChange("lookupEditVersion", ISOString);
}
private renderTierInput() {
const { isEdit, lookupTier, allLookupTiers, onChange } = this.props;
if (isEdit) {
return <FormGroup className={"lookup-label"} label={"Tier: "}>
<InputGroup
value={lookupTier}
onChange={(e: any) => onChange("lookupEditTier", e.target.value)}
disabled={true}
/>
</FormGroup>
} else {
return <FormGroup className={"lookup-label"} label={"Tier:"}>
<div className="pt-select">
<select disabled={isEdit} value={lookupTier} onChange={(e:any) => onChange("lookupEditTier", e.target.value)}>
{
allLookupTiers.map(tier => {
return <option key={tier} value={tier}>{tier}</option>
})
}
</select>
</div>
</FormGroup>
}
}
render() {
const { isOpen, onClose, onSubmit, lookupSpec, lookupTier, lookupName, lookupVersion, onChange, isEdit, allLookupTiers } = this.props;
const disableSubmit = lookupName === "" || lookupVersion === "" ||
lookupTier === "" || !validJson(lookupSpec);
return <Dialog
className={"lookup-edit-dialog"}
isOpen={isOpen}
onClose={onClose}
title={isEdit ? "Edit lookup" : "Add lookup"}
>
<FormGroup className={"lookup-label"} label={"Name: "}>
<InputGroup
value={lookupName}
onChange={(e: any) => onChange("lookupEditName", e.target.value)}
disabled={isEdit}
placeholder={"Enter the lookup name"}
/>
</FormGroup>
{ this.renderTierInput() }
<FormGroup className={"lookup-label"} label={"Version:"}>
<InputGroup
value={lookupVersion}
onChange={(e: any) => onChange("lookupEditVersion", e.target.value)}
placeholder={"Enter the lookup version"}
rightElement={<Button className={"pt-minimal"} text={"Use ISO as version"} onClick={() => this.addISOVersion()} />}
/>
</FormGroup>
<FormGroup className={"lookup-label"} label={"Spec:"}/>
<AceEditor
className={"lookup-edit-dialog-textarea"}
mode="sql"
theme="solarized_dark"
onChange={
(e: any) => onChange("lookupEditSpec", e)
}
fontSize={12}
height={"40vh"}
width={"auto"}
showPrintMargin={false}
showGutter={false}
value={lookupSpec}
editorProps={{$blockScrolling: Infinity}}
setOptions={{
enableBasicAutocompletion: false,
enableLiveAutocompletion: false,
tabSize: 2,
}}
/>
<div className={Classes.DIALOG_FOOTER}>
<div className={Classes.DIALOG_FOOTER_ACTIONS}>
<Button
text="Close"
onClick={onClose}
/>
<Button
text="Submit"
intent={Intent.PRIMARY}
onClick={() => onSubmit()}
disabled={disableSubmit}
/>
</div>
</div>
</Dialog>;
}
}

View File

@ -137,3 +137,14 @@ export function localStorageGet(key: string): string | null {
if (typeof localStorage === 'undefined') return null;
return localStorage.getItem(key);
}
// ----------------------------
export function validJson(json: string): boolean {
try {
JSON.parse(json);
return true;
} catch (e) {
return false;
}
}

View File

@ -0,0 +1,34 @@
/*
* 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.
*/
.lookups-view {
height: 100%;
width: 100%;
.ReactTable {
position: absolute;
top: 60px;
bottom: 0;
width: 100%;
}
.init-div {
text-align: center;
margin-top: 35vh;
}
}

View File

@ -0,0 +1,306 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
* distributed with this work for additional information
* regarding copyright ownership. The ASF licenses this file
* to you under the Apache License, Version 2.0 (the
* "License"); you may not use this file except in compliance
* with the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import axios from 'axios';
import * as React from 'react';
import * as classNames from 'classnames';
import ReactTable from "react-table";
import { Filter } from "react-table";
import { Button, Intent } from "@blueprintjs/core";
import {getDruidErrorMessage, QueryManager} from "../utils";
import {LookupEditDialog} from "../dialogs/lookup-edit-dialog";
import { AppToaster } from "../singletons/toaster";
import "./lookups-view.scss";
export interface LookupsViewProps extends React.Props<any> {
}
export interface LookupsViewState {
lookups: {}[],
loadingLookups: boolean,
lookupsError: string | null,
lookupEditDialogOpen: boolean,
lookupEditName: string,
lookupEditTier: string,
lookupEditVersion: string,
lookupEditSpec: string,
isEdit: boolean,
allLookupTiers: string[]
}
export class LookupsView extends React.Component<LookupsViewProps, LookupsViewState> {
private lookupsGetQueryManager: QueryManager<string, {lookupEntries: any[], tiers: string[]}>;
private lookupDeleteQueryManager: QueryManager<string, any[]>;
constructor(props: LookupsViewProps, context: any) {
super(props, context);
this.state = {
lookups: [],
loadingLookups: true,
lookupsError: null,
lookupEditDialogOpen: false,
lookupEditTier: "",
lookupEditName: "",
lookupEditVersion: "",
lookupEditSpec: "",
isEdit: false,
allLookupTiers: []
};
}
componentDidMount(): void {
this.lookupsGetQueryManager = new QueryManager({
processQuery: async (query: string) => {
const tiersResp = await axios.get('/druid/coordinator/v1/tiers');
const tiers = tiersResp.data;
let lookupEntries: {}[] = [];
const lookupResp = await axios.get("/druid/coordinator/v1/lookups/config/all");
const lookupData = lookupResp.data;
Object.keys(lookupData).map((tier: string) => {
const lookupIds = lookupData[tier];
Object.keys(lookupIds).map((id: string) => {
lookupEntries.push({tier: tier, id: id, version:lookupData[tier][id].version, spec: lookupData[tier][id].lookupExtractorFactory},);
})
})
return {
lookupEntries,
tiers
};
},
onStateChange: ({ result, loading, error }) => {
this.setState({
lookups: result === null ? [] : result.lookupEntries,
loadingLookups: loading,
lookupsError: error,
allLookupTiers: result === null ? [] : result.tiers
});
}
});
this.lookupsGetQueryManager.runQuery("dummy");
this.lookupDeleteQueryManager = new QueryManager({
processQuery: async (url: string) => {
const lookupDeleteResp = await axios.delete(url);
return lookupDeleteResp.data;
},
onStateChange: ({}) => {
this.lookupsGetQueryManager.rerunLastQuery();
}
});
}
componentWillUnmount(): void {
this.lookupsGetQueryManager.terminate();
this.lookupDeleteQueryManager.terminate();
}
private async initializeLookup() {
try {
await axios.post(`/druid/coordinator/v1/lookups/config`, {});
this.lookupsGetQueryManager.rerunLastQuery();
} catch (e) {
AppToaster.show(
{
iconName: 'error',
intent: Intent.DANGER,
message: getDruidErrorMessage(e)
}
)
}
}
private async openLookupEditDialog(tier:string, id: string) {
const { lookups, allLookupTiers } = this.state;
const target: any = lookups.find((lookupEntry: any) => {
return lookupEntry.tier === tier && lookupEntry.id === id;
});
if (id === "") {
this.setState({
lookupEditName: "",
lookupEditTier: allLookupTiers[0],
lookupEditDialogOpen: true,
lookupEditSpec: "",
lookupEditVersion: (new Date()).toISOString(),
isEdit: false
});
} else {
this.setState({
lookupEditName: id,
lookupEditTier: tier,
lookupEditDialogOpen: true,
lookupEditSpec: JSON.stringify(target.spec, null, 2),
lookupEditVersion: target.version,
isEdit: true
});
}
}
private changeLookup(field: string, value: string) {
this.setState({
[field]: value
} as any)
}
private async submitLookupEdit() {
const { lookupEditTier, lookupEditName, lookupEditSpec, lookupEditVersion, isEdit } = this.state;
let endpoint = "/druid/coordinator/v1/lookups/config";
const specJSON: any = JSON.parse(lookupEditSpec);
let dataJSON: any;
if (isEdit) {
endpoint = `${endpoint}/${lookupEditTier}/${lookupEditName}`;
dataJSON = {
version: lookupEditVersion,
lookupExtractorFactory: specJSON
};
} else {
dataJSON = {
[lookupEditTier]: {
[lookupEditName]: {
version: lookupEditVersion,
lookupExtractorFactory: specJSON
}
}
};
}
try {
await axios.post(endpoint, dataJSON);
this.setState({
lookupEditDialogOpen: false
})
this.lookupsGetQueryManager.rerunLastQuery();
} catch(e) {
AppToaster.show(
{
iconName: 'error',
intent: Intent.DANGER,
message: getDruidErrorMessage(e)
}
)
}
}
private deleteLookup(tier:string, name: string): void {
const url = `/druid/coordinator/v1/lookups/config/${tier}/${name}`;
this.lookupDeleteQueryManager.runQuery(url);
}
renderLookupsTable() {
const { lookups, loadingLookups, lookupsError} = this.state;
if (lookupsError) {
return <div className={"init-div"}>
<Button
iconName="build"
text="Initialize Lookup"
onClick={() => this.initializeLookup()}
/>
</div>
}
return <>
<ReactTable
data={lookups}
loading={loadingLookups}
noDataText={!loadingLookups && lookups && !lookups.length ? 'No lookups' : (lookupsError || '')}
filterable={true}
columns={[
{
Header: "Lookup Name",
id: "lookup_name",
accessor: (row: any) => row.id,
filterable: true,
},
{
Header: "Tier",
id: "tier",
accessor: (row: any) => row.tier,
filterable: true,
},
{
Header: "Type",
id: "type",
accessor: (row: any) => row.spec.type,
filterable: true,
},
{
Header: "Version",
id: "version",
accessor: (row: any) => row.version,
filterable: true,
},
{
Header: "Config",
id: "config",
accessor: row => {return {id: row.id, tier: row.tier};},
filterable: false,
Cell: (row: any) => {
const lookupId = row.value.id;
const lookupTier = row.value.tier;
return <div>
<a onClick={() => this.openLookupEditDialog(lookupTier,lookupId)}>Edit</a>
&nbsp;&nbsp;&nbsp;
<a onClick={() => this.deleteLookup(lookupTier,lookupId)}>Delete</a>
</div>
}
}
]}
defaultPageSize={50}
className="-striped -highlight"
/>
</>;
}
renderLookupEditDialog () {
const { lookupEditDialogOpen, allLookupTiers, lookupEditSpec, lookupEditTier, lookupEditName, lookupEditVersion, isEdit } = this.state
return <LookupEditDialog
isOpen={lookupEditDialogOpen}
onClose={() => this.setState({ lookupEditDialogOpen: false })}
onSubmit={() => this.submitLookupEdit()}
onChange={(field: string, value: string) => this.changeLookup(field, value)}
lookupSpec= {lookupEditSpec}
lookupName={lookupEditName}
lookupTier={lookupEditTier}
lookupVersion = {lookupEditVersion}
isEdit={isEdit}
allLookupTiers={allLookupTiers}
/>
}
render() {
return <div className="lookups-view app-view">
<div className="control-bar">
<div className="control-label">Lookups</div>
<Button
iconName="refresh"
text="Refresh"
onClick={() => this.lookupsGetQueryManager.rerunLastQuery()}
/>
<Button
iconName="plus"
text="Add"
style={{display: this.state.lookupsError !== null ? 'none' : 'inline'}}
onClick={() => this.openLookupEditDialog("", "")}
/>
</div>
{this.renderLookupsTable()}
{this.renderLookupEditDialog()}
</div>
}
}