diff --git a/web-console/src/components/auto-form.scss b/web-console/src/components/auto-form.scss new file mode 100644 index 000000000000..c27406c1983f --- /dev/null +++ b/web-console/src/components/auto-form.scss @@ -0,0 +1,23 @@ +/* + * 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. + */ + +.auto-form { + .ace_scroller { + background-color: #212c36; + } +} \ No newline at end of file diff --git a/web-console/src/components/auto-form.tsx b/web-console/src/components/auto-form.tsx index 6f86a4141c09..686a267b48a5 100644 --- a/web-console/src/components/auto-form.tsx +++ b/web-console/src/components/auto-form.tsx @@ -19,12 +19,14 @@ import { InputGroup } from "@blueprintjs/core"; import * as React from 'react'; -import { FormGroup, HTMLSelect, NumericInput, TagInput } from "../components/filler"; +import { FormGroup, HTMLSelect, JSONInput, NumericInput, TagInput } from "../components/filler"; + +import "./auto-form.scss"; interface Field { name: string; label?: string; - type: 'number' | 'size-bytes' | 'string' | 'boolean' | 'string-array'; + type: 'number' | 'size-bytes' | 'string' | 'boolean' | 'string-array' | 'json'; min?: number; } @@ -32,9 +34,11 @@ export interface AutoFormProps extends React.Props { fields: Field[]; model: T | null; onChange: (newValue: T) => void; + updateJSONValidity?: (jsonValidity: boolean) => void; } export interface AutoFormState { + jsonInputsValidity: any; } export class AutoForm extends React.Component, AutoFormState> { @@ -47,6 +51,7 @@ export class AutoForm extends React.Component, AutoFormState constructor(props: AutoFormProps) { super(props); this.state = { + jsonInputsValidity: {} }; } @@ -99,9 +104,30 @@ export class AutoForm extends React.Component, AutoFormState ; } + private renderJSONInput(field: Field): JSX.Element { + const { model, onChange, updateJSONValidity } = this.props; + const { jsonInputsValidity } = this.state; + + const updateInputValidity = (e: any) => { + if (updateJSONValidity) { + const newJSONInputValidity = Object.assign({}, jsonInputsValidity, { [field.name]: e}); + this.setState({ + jsonInputsValidity: newJSONInputValidity + }); + const allJSONValid: boolean = Object.keys(newJSONInputValidity).every(property => newJSONInputValidity[property] === true); + updateJSONValidity(allJSONValid); + } + }; + + return onChange(Object.assign({}, model, { [field.name]: e}))} + updateInputValidity={updateInputValidity} + />; + } + private renderStringArrayInput(field: Field): JSX.Element { const { model, onChange } = this.props; - const label = field.label || AutoForm.makeLabelName(field.name); return { @@ -118,6 +144,7 @@ export class AutoForm extends React.Component, AutoFormState case 'string': return this.renderStringInput(field); case 'boolean': return this.renderBooleanInput(field); case 'string-array': return this.renderStringArrayInput(field); + case 'json': return this.renderJSONInput(field); default: throw new Error(`unknown field type '${field.type}'`); } } @@ -131,7 +158,6 @@ export class AutoForm extends React.Component, AutoFormState render() { const { fields, model } = this.props; - return
{model && fields.map(field => this.renderField(field))}
; diff --git a/web-console/src/components/filler.tsx b/web-console/src/components/filler.tsx index e20cd7777539..11dcf9355a9b 100644 --- a/web-console/src/components/filler.tsx +++ b/web-console/src/components/filler.tsx @@ -19,6 +19,9 @@ import { Button } from '@blueprintjs/core'; import classNames from 'classnames'; import * as React from 'react'; +import AceEditor from "react-ace"; + +import { parseStringToJSON, stringifyJSON, validJson } from "../utils"; import './filler.scss'; @@ -258,3 +261,71 @@ export class TagInput extends React.Component; } } + +interface JSONInputProps extends React.Props { + onChange: (newJSONValue: any) => void; + value: any; + updateInputValidity: (valueValid: boolean) => void; +} + +interface JSONInputState { + stringValue: string; +} + +export class JSONInput extends React.Component { + constructor(props: JSONInputProps) { + super(props); + this.state = { + stringValue: "" + }; + } + + componentDidMount(): void { + const { value } = this.props; + const stringValue = stringifyJSON(value); + this.setState({ + stringValue + }); + } + + componentWillReceiveProps(nextProps: JSONInputProps): void { + if (JSON.stringify(nextProps.value) !== JSON.stringify(this.props.value)) { + this.setState({ + stringValue: stringifyJSON(nextProps.value) + }); + } + } + + render() { + const { onChange, updateInputValidity } = this.props; + const { stringValue } = this.state; + return { + this.setState({stringValue: e}); + if (validJson(e) || e === "") onChange(parseStringToJSON(e)); + updateInputValidity(validJson(e) || e === ''); + }} + focus + fontSize={12} + width={'100%'} + height={"8vh"} + showPrintMargin={false} + showGutter={false} + value={stringValue} + editorProps={{ + $blockScrolling: Infinity + }} + setOptions={{ + enableBasicAutocompletion: false, + enableLiveAutocompletion: false, + showLineNumbers: false, + tabSize: 2 + }} + />; + } +} diff --git a/web-console/src/dialogs/compaction-dialog.scss b/web-console/src/dialogs/compaction-dialog.scss new file mode 100644 index 000000000000..ee1cf675b956 --- /dev/null +++ b/web-console/src/dialogs/compaction-dialog.scss @@ -0,0 +1,30 @@ +/* + * 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. + */ + +.compaction-dialog { + &.pt-dialog { + top: 5%; + } + + .auto-form { + margin: 10px 15px; + padding: 0 5px 0 5px; + max-height: 70vh; + overflow: scroll; + } +} diff --git a/web-console/src/dialogs/compaction-dialog.tsx b/web-console/src/dialogs/compaction-dialog.tsx new file mode 100644 index 000000000000..d5006cd39f32 --- /dev/null +++ b/web-console/src/dialogs/compaction-dialog.tsx @@ -0,0 +1,140 @@ +/* + * 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, Classes, Dialog, Intent } from "@blueprintjs/core"; +import * as React from 'react'; + +import { AutoForm } from '../components/auto-form'; + +import './compaction-dialog.scss'; + +export interface CompactionDialogProps extends React.Props { + onClose: () => void; + onSave: (config: any) => void; + onDelete: () => void; + datasource: string; + configData: any; +} + +export interface CompactionDialogState { + currentConfig: Record | null; + allJSONValid: boolean; +} + +export class CompactionDialog extends React.Component { + constructor(props: CompactionDialogProps) { + super(props); + this.state = { + currentConfig: null, + allJSONValid: true + }; + } + + componentDidMount(): void { + const { datasource, configData } = this.props; + let config: Record = { + dataSource: datasource, + inputSegmentSizeBytes: 419430400, + keepSegmentGranularity: true, + maxNumSegmentsToCompact: 150, + skipOffsetFromLatest: "P1D", + targetCompactionSizeBytes: 419430400, + taskContext: null, + taskPriority: 25, + tuningConfig: null + }; + if (configData !== undefined) { + config = configData; + } + this.setState({ + currentConfig: config + }); + } + + render() { + const { onClose, onSave, onDelete, datasource, configData } = this.props; + const { currentConfig, allJSONValid } = this.state; + return + this.setState({currentConfig: m})} + updateJSONValidity={e => this.setState({allJSONValid: e})} + /> +
+
+
+
+
; + } +} diff --git a/web-console/src/dialogs/retention-dialog.scss b/web-console/src/dialogs/retention-dialog.scss index 7c9b51d0616f..eab743bcf060 100644 --- a/web-console/src/dialogs/retention-dialog.scss +++ b/web-console/src/dialogs/retention-dialog.scss @@ -17,11 +17,10 @@ */ .retention-dialog { - width: 750px; - position: absolute; - top: 50%; - left: 50%; - transform: translate(-50%, -50%) !important; + &.pt-dialog { + top: 5%; + width: 750px; + } .dialog-body { overflow: scroll; diff --git a/web-console/src/utils/general.tsx b/web-console/src/utils/general.tsx index 7fce2791abaa..6867faf97f11 100644 --- a/web-console/src/utils/general.tsx +++ b/web-console/src/utils/general.tsx @@ -148,3 +148,21 @@ export function validJson(json: string): boolean { return false; } } + +// stringify JSON to string; if JSON is null, parse empty string "" +export function stringifyJSON(item: any): string { + if (item != null) { + return JSON.stringify(item, null, 2); + } else { + return ""; + } +} + +// parse string to JSON object; if string is empty, return null +export function parseStringToJSON(s: string): JSON | null { + if (s === "") { + return null; + } else { + return JSON.parse(s); + } +} diff --git a/web-console/src/views/datasource-view.tsx b/web-console/src/views/datasource-view.tsx index 1deae85fcc64..3ee04a237f9c 100644 --- a/web-console/src/views/datasource-view.tsx +++ b/web-console/src/views/datasource-view.tsx @@ -18,14 +18,13 @@ import { Button, Intent, Switch } from "@blueprintjs/core"; import axios from 'axios'; -import * as classNames from 'classnames'; import * as React from 'react'; -import ReactTable from "react-table"; -import { Filter } from "react-table"; +import ReactTable, { Filter } from "react-table"; import { IconNames } from "../components/filler"; import { RuleEditor } from '../components/rule-editor'; import { AsyncActionDialog } from '../dialogs/async-action-dialog'; +import { CompactionDialog } from "../dialogs/compaction-dialog"; import { RetentionDialog } from '../dialogs/retention-dialog'; import { AppToaster } from '../singletons/toaster'; import { @@ -61,6 +60,7 @@ export interface DatasourcesViewState { showDisabled: boolean; retentionDialogOpenOn: { datasource: string, rules: any[] } | null; + compactionDialogOpenOn: {datasource: string, configData: any} | null; dropDataDatasource: string | null; enableDatasource: string | null; killDatasource: string | null; @@ -95,6 +95,7 @@ export class DatasourcesView extends React.Component { + if (compactionConfig === null) return; + try { + await axios.post(`/druid/coordinator/v1/config/compaction`, compactionConfig); + this.setState({compactionDialogOpenOn: null}); + this.datasourceQueryManager.rerunLastQuery(); + } catch (e) { + AppToaster.show({ + message: e, + intent: Intent.DANGER + }); + } + } + + private deleteCompaction = async () => { + const {compactionDialogOpenOn} = this.state; + if (compactionDialogOpenOn === null) return; + const datasource = compactionDialogOpenOn.datasource; + AppToaster.show({ + message: `Are you sure you want to delete ${datasource}'s compaction?`, + intent: Intent.DANGER, + action: { + text: "Confirm", + onClick: async () => { + try { + await axios.delete(`/druid/coordinator/v1/config/compaction/${datasource}`); + this.setState({compactionDialogOpenOn: null}, () => this.datasourceQueryManager.rerunLastQuery()); + } catch (e) { + AppToaster.show({ + message: e, + intent: Intent.DANGER + }); + } + } + } + }); + } + renderRetentionDialog() { const { retentionDialogOpenOn, tiers } = this.state; if (!retentionDialogOpenOn) return null; @@ -286,6 +325,20 @@ GROUP BY 1`); />; } + renderCompactionDialog() { + const { datasources, compactionDialogOpenOn } = this.state; + + if (!compactionDialogOpenOn || !datasources) return; + + return this.setState({compactionDialogOpenOn: null})} + onSave={this.saveCompaction} + onDelete={this.deleteCompaction} + />; + } + renderDatasourceTable() { const { goToSegments } = this.props; const { datasources, defaultRules, datasourcesLoading, datasourcesError, datasourcesFilter, showDisabled } = this.state; @@ -379,15 +432,24 @@ GROUP BY 1`); filterable: false, Cell: row => { const { compaction } = row.original; + const compactionOpenOn: {datasource: string, configData: any} | null = { + datasource: row.original.datasource, + configData: compaction + }; let text: string; if (compaction) { text = `Target: ${formatBytes(compaction.targetCompactionSizeBytes)}`; } else { text = 'None'; } - return {text} alert('ToDo')}>✎; - }, - show: false // This feature is not ready, it will be enabled later + return this.setState({compactionDialogOpenOn: compactionOpenOn})} + > + {text}  + + ; + } }, { Header: 'Size', @@ -432,6 +494,7 @@ GROUP BY 1`); {this.renderEnableAction()} {this.renderKillAction()} {this.renderRetentionDialog()} + {this.renderCompactionDialog()} ; }