From 1b6b40e5117772988885b8125b34c53a1e9c147a Mon Sep 17 00:00:00 2001 From: Qi Shu Date: Sat, 16 Mar 2019 01:46:43 -0700 Subject: [PATCH] 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 + .../src/dialogs/lookup-edit-dialog.scss | 41 +++ .../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(-) create mode 100644 web-console/src/dialogs/lookup-edit-dialog.scss create mode 100644 web-console/src/dialogs/lookup-edit-dialog.tsx create mode 100644 web-console/src/views/lookups-view.scss create mode 100644 web-console/src/views/lookups-view.tsx diff --git a/web-console/src/components/filler.tsx b/web-console/src/components/filler.tsx index 30809b85295..a0685f60d52 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 892f229c084..b4e4dad2bbf 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 { active: HeaderActiveTab; @@ -100,6 +100,7 @@ export class HeaderBar extends React.Component { const configMenu = this.setState({ coordinatorDynamicConfigDialogOpen: true })}/> + ; return diff --git a/web-console/src/console-application.tsx b/web-console/src/console-application.tsx index 29ab1300c5a..9b610526a91 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 { @@ -156,6 +157,9 @@ export class ConsoleApplication extends React.Component { return wrapInViewContainer('sql', ); }} /> + { + return wrapInViewContainer('lookups', ); + }} /> { return wrapInViewContainer(null, ) }} /> 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 00000000000..0e5b2d67dbe --- /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 00000000000..3375b943fd4 --- /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 { + 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 { + + 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 + onChange("lookupEditTier", e.target.value)} + disabled={true} + /> + + } else { + return +
+ +
+
+ } + } + + render() { + const { isOpen, onClose, onSubmit, lookupSpec, lookupTier, lookupName, lookupVersion, onChange, isEdit, allLookupTiers } = this.props; + + const disableSubmit = lookupName === "" || lookupVersion === "" || + lookupTier === "" || !validJson(lookupSpec); + + return + + onChange("lookupEditName", e.target.value)} + disabled={isEdit} + placeholder={"Enter the lookup name"} + /> + + + { this.renderTierInput() } + + + onChange("lookupEditVersion", e.target.value)} + placeholder={"Enter the lookup version"} + rightElement={; + } +} \ No newline at end of file diff --git a/web-console/src/utils/general.tsx b/web-console/src/utils/general.tsx index a64015d3c9a..a40ebad0d5f 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 00000000000..4fc35dc4ffe --- /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 00000000000..f7e9fe6c52d --- /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 { + +} + +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 { + private lookupsGetQueryManager: QueryManager; + private lookupDeleteQueryManager: QueryManager; + + 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
+
+ } + return <> + 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 + } + } + ]} + defaultPageSize={50} + className="-striped -highlight" + /> + ; + } + + renderLookupEditDialog () { + const { lookupEditDialogOpen, allLookupTiers, lookupEditSpec, lookupEditTier, lookupEditName, lookupEditVersion, isEdit } = this.state + + return 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
+
+
Lookups
+
+ {this.renderLookupsTable()} + {this.renderLookupEditDialog()} +
+ } +} \ No newline at end of file