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>
+                &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>
+  }
+}
\ No newline at end of file


---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]

Reply via email to