This is an automated email from the ASF dual-hosted git repository.
cwylie pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-druid.git
The following commit(s) were added to refs/heads/master by this push:
new 1b6b40e Add lookups view to allow adding/editing/deleting of druid
lookups (#7259)
1b6b40e is described below
commit 1b6b40e5117772988885b8125b34c53a1e9c147a
Author: Qi Shu <[email protected]>
AuthorDate: Sat Mar 16 01:46:43 2019 -0700
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
---
web-console/src/components/filler.tsx | 2 +
web-console/src/components/header-bar.tsx | 3 +-
web-console/src/console-application.tsx | 4 +
web-console/src/dialogs/lookup-edit-dialog.scss | 41 ++++
web-console/src/dialogs/lookup-edit-dialog.tsx | 154 ++++++++++++
web-console/src/utils/general.tsx | 11 +
web-console/src/views/lookups-view.scss | 34 +++
web-console/src/views/lookups-view.tsx | 306 ++++++++++++++++++++++++
8 files changed, 554 insertions(+), 1 deletion(-)
diff --git a/web-console/src/components/filler.tsx
b/web-console/src/components/filler.tsx
index 30809b8..a0685f6 100644
--- a/web-console/src/components/filler.tsx
+++ b/web-console/src/components/filler.tsx
@@ -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];
diff --git a/web-console/src/components/header-bar.tsx
b/web-console/src/components/header-bar.tsx
index 892f229..b4e4dad 100644
--- a/web-console/src/components/header-bar.tsx
+++ b/web-console/src/components/header-bar.tsx
@@ -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">
diff --git a/web-console/src/console-application.tsx
b/web-console/src/console-application.tsx
index 29ab130..9b61052 100644
--- a/web-console/src/console-application.tsx
+++ b/web-console/src/console-application.tsx
@@ -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/>)
}} />
diff --git a/web-console/src/dialogs/lookup-edit-dialog.scss
b/web-console/src/dialogs/lookup-edit-dialog.scss
new file mode 100644
index 0000000..0e5b2d6
--- /dev/null
+++ b/web-console/src/dialogs/lookup-edit-dialog.scss
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/web-console/src/dialogs/lookup-edit-dialog.tsx
b/web-console/src/dialogs/lookup-edit-dialog.tsx
new file mode 100644
index 0000000..3375b94
--- /dev/null
+++ b/web-console/src/dialogs/lookup-edit-dialog.tsx
@@ -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>;
+ }
+}
\ No newline at end of file
diff --git a/web-console/src/utils/general.tsx
b/web-console/src/utils/general.tsx
index a64015d..a40ebad 100644
--- a/web-console/src/utils/general.tsx
+++ b/web-console/src/utils/general.tsx
@@ -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;
+ }
+}
diff --git a/web-console/src/views/lookups-view.scss
b/web-console/src/views/lookups-view.scss
new file mode 100644
index 0000000..4fc35dc
--- /dev/null
+++ b/web-console/src/views/lookups-view.scss
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/web-console/src/views/lookups-view.tsx
b/web-console/src/views/lookups-view.tsx
new file mode 100644
index 0000000..f7e9fe6
--- /dev/null
+++ b/web-console/src/views/lookups-view.tsx
@@ -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>
+
+ <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>
+ }
+}
\ No newline at end of file
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]