You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pinot.apache.org by ne...@apache.org on 2020/12/03 21:51:31 UTC
[incubator-pinot] branch master updated: support to add offline and
realtime tables,
individually able to add schema and schema listing in UI (#6296)
This is an automated email from the ASF dual-hosted git repository.
nehapawar pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/incubator-pinot.git
The following commit(s) were added to refs/heads/master by this push:
new f898c18 support to add offline and realtime tables, individually able to add schema and schema listing in UI (#6296)
f898c18 is described below
commit f898c1813ee2678e77c7dcc96194c64f441cd713
Author: Sanket Shah <sh...@users.noreply.github.com>
AuthorDate: Fri Dec 4 03:21:14 2020 +0530
support to add offline and realtime tables, individually able to add schema and schema listing in UI (#6296)
* support to add offline and realtime tables, individually able to add schema and schema listing in UI
* fixing issues
* show multi-value only for dimension
* additional ui fixes and added schema edit and delete option in schema detail page
* show asterix for required fields and fix issue where loader is always spinning
---
.../main/resources/app/components/Breadcrumbs.tsx | 9 +-
.../Homepage/Operations/AddDeleteComponent.tsx | 174 +++++++++++
.../Homepage/Operations/AddIndexingComponent.tsx | 161 ++++++++++
.../Homepage/Operations/AddIngestionComponent.tsx | 169 ++++++++++
...{AddTableSchemaOp.tsx => AddOfflineTableOp.tsx} | 235 ++++++++++----
.../Operations/AddOfflineTenantComponent.tsx | 151 +++++++++
.../Homepage/Operations/AddPartionComponent.tsx | 259 ++++++++++++++++
.../Homepage/Operations/AddQueryComponent.tsx | 89 ++++++
.../Operations/AddRealTimeIngestionComponent.tsx | 167 ++++++++++
.../Operations/AddRealTimePartionComponent.tsx | 230 ++++++++++++++
...Component.tsx => AddRealtimeTableComponent.tsx} | 9 +-
.../{AddTableOp.tsx => AddRealtimeTableOp.tsx} | 161 +++++++++-
.../components/Homepage/Operations/AddSchemaOp.tsx | 203 ++++++++++++
.../Homepage/Operations/AddStorageComponent.tsx | 117 +++++++
.../Homepage/Operations/AddTableComponent.tsx | 8 +-
.../Homepage/Operations/AddTenantComponent.tsx | 172 +++++++++++
.../Homepage/Operations/MultiIndexingComponent.tsx | 344 +++++++++++++++++++++
.../Homepage/Operations/MultiMetricComponent.tsx | 182 +++++++++++
.../Operations/MultipleSelectComponent.tsx | 185 +++++++++++
.../Homepage/Operations/SchemaComponent.tsx | 44 ++-
.../Homepage/Operations/SchemaNameComponent.tsx | 104 +++++++
.../src/main/resources/app/interfaces/types.d.ts | 8 +
.../main/resources/app/pages/SchemaPageDetails.tsx | 308 ++++++++++++++++++
.../main/resources/app/pages/TablesListingPage.tsx | 65 +++-
.../src/main/resources/app/pages/TenantDetails.tsx | 2 +-
.../src/main/resources/app/requests/index.ts | 3 +
pinot-controller/src/main/resources/app/router.tsx | 2 +
.../src/main/resources/app/styles/styles.css | 33 ++
.../main/resources/app/utils/PinotMethodUtils.ts | 42 ++-
29 files changed, 3532 insertions(+), 104 deletions(-)
diff --git a/pinot-controller/src/main/resources/app/components/Breadcrumbs.tsx b/pinot-controller/src/main/resources/app/components/Breadcrumbs.tsx
index 1cf97ef..db05202 100644
--- a/pinot-controller/src/main/resources/app/components/Breadcrumbs.tsx
+++ b/pinot-controller/src/main/resources/app/components/Breadcrumbs.tsx
@@ -91,7 +91,7 @@ const BreadcrumbsComponent = ({ ...props }) => {
const breadcrumbs = [getClickableLabel(breadcrumbNameMap['/'], '/')];
const paramsKeys = _.keys(props.match.params);
if(paramsKeys.length){
- const {tenantName, tableName, segmentName, instanceName} = props.match.params;
+ const {tenantName, tableName, segmentName, instanceName, schemaName} = props.match.params;
if((tenantName || instanceName) && tableName){
breadcrumbs.push(
getClickableLabel(
@@ -113,7 +113,12 @@ const BreadcrumbsComponent = ({ ...props }) => {
getClickableLabel('Tables', '/tables')
);
}
- breadcrumbs.push(getLabel(segmentName || tableName || tenantName || instanceName));
+ if(schemaName){
+ breadcrumbs.push(
+ getClickableLabel('Schemas', '/tables')
+ );
+ }
+ breadcrumbs.push(getLabel(segmentName || tableName || tenantName || instanceName || schemaName));
} else {
breadcrumbs.push(getLabel(breadcrumbNameMap[location.pathname]));
}
diff --git a/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddDeleteComponent.tsx b/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddDeleteComponent.tsx
new file mode 100644
index 0000000..c1d5571
--- /dev/null
+++ b/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddDeleteComponent.tsx
@@ -0,0 +1,174 @@
+/**
+ * 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 React, { useEffect, useState } from 'react';
+import { createStyles, FormControl, Grid, Input, InputLabel, makeStyles, MenuItem, Select, TextField, Theme, IconButton, Fab, Button} from '@material-ui/core';
+import AddIcon from '@material-ui/icons/Add';
+import AddCircleIcon from '@material-ui/icons/AddCircle';
+import ClearIcon from '@material-ui/icons/Clear';
+import DeleteIcon from '@material-ui/icons/Delete';
+
+const useStyles = makeStyles((theme: Theme) =>
+ createStyles({
+ formControl: {
+ margin: theme.spacing(1),
+ width: '100%'
+ },
+ deleteIcon:{
+ marginTop:15,
+ marginLeft:-15,
+ color: theme.palette.error.main
+ },
+ selectFormControl: {
+ margin: theme.spacing(1),
+ width: 170
+ },
+ autoCompleteControl: {
+ '& .MuiFormControl-marginNormal': {
+ marginTop: 0
+ }
+ },
+ redColor: {
+ color: theme.palette.error.main
+ }
+ })
+);
+
+type Props = {
+ changeHandler: any,
+ streamConfigsObj: Object
+};
+
+const compulsoryKeys = ["stream.kafka.broker.list","stream.kafka.topic.name","stream.kafka.consumer.type","stream.kafka.decoder.class.name"];
+
+export default function AddDeleteComponent({
+ changeHandler,
+ streamConfigsObj
+}: Props) {
+ const classes = useStyles();
+ const [streamConfigObj, setStreamConfigObj] = useState(streamConfigsObj);
+ const [keys,setKeys] = useState(Object.keys(streamConfigObj));
+ const [value,setValue] = useState(Object.values(streamConfigObj));
+
+ const addButtonClick = ()=>{
+ let data = "";
+ let tempStreamConfigObj = {...streamConfigObj,
+ [data]:data}
+ setStreamConfigObj(tempStreamConfigObj);
+ changeHandler('streamConfigs',tempStreamConfigObj)
+ }
+
+ const keyChange = (input,index, value) =>{
+ input[index] = value;
+ setKeys([...input]);
+ }
+
+ const valueChange = (input,index, value) =>{
+ input[index] = value;
+ setValue([...input]);
+ }
+
+ const updateJson = () =>{
+ let configObj = {}
+ keys.map((k,i)=>{
+ configObj[k] = value[i]
+ })
+ changeHandler('streamConfigs',configObj);
+ }
+
+ const deleteClick = (val) =>{
+ let tempStreamConfigObj = {...streamConfigObj};
+ delete tempStreamConfigObj[val];
+ setStreamConfigObj(tempStreamConfigObj);
+ setKeys([]);
+ setValue([]);
+ changeHandler('streamConfigs',tempStreamConfigObj)
+ }
+
+ useEffect(() => {
+ setStreamConfigObj(streamConfigsObj);
+ setKeys(Object.keys(streamConfigObj));
+ setValue(Object.values(streamConfigObj));
+ }, [streamConfigsObj]);
+
+
+ const requiredAstrix = <span className={classes.redColor}>*</span>;
+
+ return (
+ <Grid container>
+ <h3 className="accordion-subtitle">Stream Config</h3>
+ {
+ keys.map((o,i)=>{
+ return(
+ <Grid item xs={6}>
+ <div className="box-border">
+ <Grid container spacing={2}>
+ <Grid item xs={6}>
+ <FormControl className={classes.formControl}>
+ <InputLabel htmlFor={o}>Key { compulsoryKeys.includes(o) && requiredAstrix }</InputLabel>
+ <Input
+ id={o}
+ value={o}
+ key={i+"key"}
+ onChange={(e)=> keyChange(keys,i,e.target.value)}
+ onBlur={updateJson}
+ />
+ </FormControl>
+ </Grid>
+ <Grid item xs={5}>
+ <FormControl className={classes.formControl}>
+ <InputLabel htmlFor={value[i]}>Value</InputLabel>
+ <Input
+ id={value[i]}
+ value={value[i]}
+ key={i+"value"}
+ onChange={(e)=> valueChange(value,i,e.target.value)}
+ onBlur={updateJson}
+ />
+ </FormControl>
+ </Grid>
+ <Grid item xs={1}>
+ <FormControl>
+ <IconButton aria-label="delete" key={"delete"+i} className={classes.deleteIcon} onClick={()=>{
+ deleteClick(o)}}>
+ <ClearIcon />
+ </IconButton>
+ </FormControl>
+ </Grid>
+ </Grid>
+ </div>
+ </Grid>)
+ })
+ }
+ <Grid item xs={3}>
+ <FormControl className={classes.formControl}>
+ <Button
+ aria-label="plus"
+ variant="outlined"
+ color="primary"
+ onClick={addButtonClick}
+ startIcon={(<AddCircleIcon />)}
+ >
+ Add new Field
+ </Button>
+ </FormControl>
+ </Grid>
+ </Grid>
+ );
+}
\ No newline at end of file
diff --git a/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddIndexingComponent.tsx b/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddIndexingComponent.tsx
new file mode 100644
index 0000000..3815264
--- /dev/null
+++ b/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddIndexingComponent.tsx
@@ -0,0 +1,161 @@
+/**
+ * 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 React, { useEffect, useState } from 'react';
+import { createStyles, Grid, makeStyles, Theme} from '@material-ui/core';
+import MultiIndexingComponent from './MultiIndexingComponent';
+
+const useStyles = makeStyles((theme: Theme) =>
+ createStyles({
+ formControl: {
+ margin: theme.spacing(1),
+ minWidth: 120,
+ },
+ selectFormControl: {
+ margin: theme.spacing(1),
+ width: 220
+ }
+ })
+);
+
+type Props = {
+ tableObj: any,
+ setTableObj: Function,
+ columnName: Array<string>
+};
+
+export default function AddIndexingComponent({
+ tableObj,
+ setTableObj,
+ columnName
+}: Props) {
+ const classes = useStyles();
+
+ const [tableDataObj, setTableDataObj] = useState(tableObj);
+ const [showTree,setShowTree] = useState(0);
+ const ITEM_HEIGHT = 48;
+ const ITEM_PADDING_TOP = 8;
+ const MenuProps = {
+ PaperProps: {
+ style: {
+ maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
+ width: 250,
+ },
+ },
+ };
+
+ const changeHandler = (fieldName, value) => {
+ let newTableObj = {...tableDataObj};
+ switch(fieldName){
+ case 'maxLeafRecords':
+ newTableObj.tableIndexConfig.starTreeIndexConfigs[0].maxLeafRecords = value;
+ // newTableObj.segmentsConfig.schemaName = value;
+ break;
+ case 'enableStarTree':
+ setShowTree(value);
+ break;
+ case 'dimensionsSplitOrder':
+ if(!newTableObj.tableIndexConfig.starTreeIndexConfigs){
+ newTableObj.tableIndexConfig.starTreeIndexConfigs = [];
+ newTableObj.tableIndexConfig.starTreeIndexConfigs[0] = {};
+ }
+ newTableObj.tableIndexConfig.starTreeIndexConfigs[0].dimensionsSplitOrder = value;
+ break;
+ case 'maxLeafRecords':
+ if(!newTableObj.tableIndexConfig.starTreeIndexConfigs){
+ newTableObj.tableIndexConfig.starTreeIndexConfigs = [];
+ newTableObj.tableIndexConfig.starTreeIndexConfigs[0] = {};
+ }
+ newTableObj.tableIndexConfig.starTreeIndexConfigs[0].maxLeafRecords = value;
+ break;
+ case 'tableIndexConfig':
+ newTableObj.tableIndexConfig = {...newTableObj.tableIndexConfig,...value.tableIndexConfig};
+ newTableObj.fieldConfigList = [...value.fieldConfigList];
+ break;
+ };
+ setTableDataObj(newTableObj);
+ setTableObj(newTableObj);
+ };
+
+ useEffect(()=>{
+ setTableDataObj(tableObj);
+ }, [tableObj]);
+
+ return (
+ <Grid container spacing={2}>
+ <Grid item xs={12}>
+ {/* <FormControl className={classes.selectFormControl}>
+ <InputLabel htmlFor="enableStarTree">Enable default star tree?</InputLabel>
+ <Select
+ labelId="enableStarTree"
+ id="enableStarTree"
+ value={showTree}
+ onChange={(e)=> changeHandler('enableStarTree', e.target.value)}
+ >
+ <MenuItem value={1}>True</MenuItem>
+ <MenuItem value={0}>False</MenuItem>
+ </Select>
+ </FormControl>
+ <FormControl className={classes.selectFormControl}>
+ <InputLabel htmlFor="dimensionsSplitOrder">Dimension split order</InputLabel>
+ <Select
+ labelId="dimensionsSplitOrder"
+ id="dimensionsSplitOrder"
+ key="dimensionsSplitOrder"
+ multiple
+ value={tableDataObj.tableIndexConfig.starTreeIndexConfigs && tableDataObj.tableIndexConfig.starTreeIndexConfigs[0].dimensionsSplitOrder || []}
+ onChange={(e)=> changeHandler('dimensionsSplitOrder', e.target.value)}
+ input={<Input id="select-multiple-chip" />}
+ renderValue={(selected) => (
+ <div className={"chips"}>
+ {(selected as string[]).map((value) => (
+ <Chip key={value} label={value} className={"chip"} />
+ ))}
+ </div>
+ )}
+ MenuProps={MenuProps}
+ >
+ <MenuItem value="Country">Country</MenuItem>
+ <MenuItem value="Browser">Browser</MenuItem>
+ <MenuItem value="Locale">Locale</MenuItem>
+ </Select>
+ </FormControl>
+ <FormControl className={classes.formControl} >
+ <InputLabel htmlFor="maxLeafRecords">Max leaf records</InputLabel>
+ <Input
+ id="maxLeafRecords"
+ value={tableDataObj.tableIndexConfig.starTreeIndexConfigs && tableDataObj.tableIndexConfig.starTreeIndexConfigs[0].maxLeafRecords}
+ onChange={(e)=> changeHandler('maxLeafRecords', e.target.value)}
+ error = {tableDataObj.tableIndexConfig.starTreeIndexConfigs && tableDataObj.tableIndexConfig.starTreeIndexConfigs[0].maxLeafRecords ? isNaN(Number(tableDataObj.tableIndexConfig.starTreeIndexConfigs[0].maxLeafRecords)) : false}
+ />
+ </FormControl> */}
+ <MultiIndexingComponent
+ key = {"multiIndex"}
+ streamConfigsObj = {{...tableDataObj.tableIndexConfig}}
+ textDataObj={tableDataObj.fieldConfigList ? [...tableDataObj.fieldConfigList] : []}
+ changeHandler = {changeHandler}
+ columnName={columnName}/>
+ {/* <MultiMetricComponent
+ key={"multiMetrix"}
+ streamConfigsObj={tableDataObj.tableIndexConfig.starTreeIndexConfigs ? {...tableDataObj.tableIndexConfig.starTreeIndexConfigs[0]} : {}}
+ changeHandler = {changeHandler}/> */}
+ </Grid>
+ </Grid>
+ );
+}
\ No newline at end of file
diff --git a/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddIngestionComponent.tsx b/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddIngestionComponent.tsx
new file mode 100644
index 0000000..5c06fa0
--- /dev/null
+++ b/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddIngestionComponent.tsx
@@ -0,0 +1,169 @@
+/**
+ * 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 React, { useEffect, useState } from 'react';
+import { createStyles, FormControl, Grid, Input, InputLabel, makeStyles, MenuItem, Select, Theme} from '@material-ui/core';
+import AddDeleteComponent from './AddDeleteComponent';
+import MultipleSelectComponent from './MultipleSelectComponent';
+import _ from 'lodash';
+
+const useStyles = makeStyles((theme: Theme) =>
+ createStyles({
+ formControl: {
+ margin: theme.spacing(1),
+ minWidth: 120,
+ },
+ selectFormControl: {
+ margin: theme.spacing(1),
+ width: 220
+ },
+ })
+);
+
+type Props = {
+ tableObj: any,
+ setTableObj: Function,
+ columnName: Array<string>
+};
+
+export default function AddIngestionComponent({
+ tableObj,
+ setTableObj,
+ columnName
+}: Props) {
+ const classes = useStyles();
+ const [newSize,setNewSize] = useState([0,1])
+ const [tableDataObj, setTableDataObj] = useState(tableObj);
+
+ const changeHandler = (fieldName, value) => {
+ let newTableObj = {...tableDataObj};
+ switch(fieldName){
+ case 'filterConfig':
+ newTableObj.ingestionConfig.filterConfig[fieldName] = value;
+ break;
+ case 'segmentPushFrequency':
+ newTableObj.segmentsConfig[fieldName] = value;
+ break;
+ case 'segmentPushType':
+ newTableObj.segmentsConfig[fieldName] = value;
+ break;
+ case 'streamConfigs':
+ newTableObj.tableIndexConfig.streamConfigs = {...value};
+ break;
+ case 'filterFunction':
+ if(!newTableObj.ingestionConfig.filterConfig){
+ newTableObj.ingestionConfig.filterConfig = {};
+ }
+ newTableObj.ingestionConfig.filterConfig.filterFunction = value;
+ break;
+ case 'transformConfigs':
+ tableDataObj.ingestionConfig.transformConfigs = value;
+ };
+ setTableDataObj(newTableObj);
+ setTableObj(newTableObj);
+ };
+
+ useEffect(()=>{
+ let newTableObj = {...tableObj};
+ if(newTableObj.tableType === "REALTIME" && !newTableObj.streamConfigs && _.isEmpty(newTableObj.streamConfigs) ){
+ newTableObj.tableIndexConfig.streamConfigs =
+ {
+ "streamType": "kafka",
+ "stream.kafka.topic.name": "",
+ "stream.kafka.broker.list": "",
+ "stream.kafka.consumer.type": "lowlevel",
+ "stream.kafka.consumer.prop.auto.offset.reset": "smallest",
+ "stream.kafka.consumer.factory.class.name":"org.apache.pinot.plugin.stream.kafka20.KafkaConsumerFactory",
+ "stream.kafka.decoder.class.name":"org.apache.pinot.plugin.stream.kafka.KafkaJSONMessageDecoder",
+ "realtime.segment.flush.threshold.rows": "0",
+ "realtime.segment.flush.threshold.time": "24h",
+ "realtime.segment.flush.segment.size": "100M"
+ }
+ setTableObj(newTableObj);
+ }else if(newTableObj.tableType !== "REALTIME" && newTableObj.streamConfigs){
+ newTableObj.streamConfigs = null;
+ setTableObj(newTableObj);
+ }
+ setTableDataObj(newTableObj);
+ }, [tableObj]);
+
+ useEffect(()=>{
+ setNewSize(newSize);
+ },[newSize])
+
+ return (
+ <Grid container spacing={2}>
+ <Grid item xs={12}>
+ {
+ tableDataObj.tableType === "OFFLINE" ?
+ <FormControl className={classes.selectFormControl}>
+ <InputLabel htmlFor="segmentPushFrequency">Offline push frequency</InputLabel>
+ <Select
+ labelId="segmentPushFrequency"
+ id="segmentPushFrequency"
+ value={tableDataObj.segmentsConfig.segmentPushFrequency !== "" ? tableDataObj.segmentsConfig.segmentPushFrequency : ""}
+ onChange={(e)=> changeHandler('segmentPushFrequency', e.target.value)}
+ >
+ <MenuItem value="HOURLY">HOURLY</MenuItem>
+ <MenuItem value="DAILY">DAILY</MenuItem>
+ </Select>
+ </FormControl> : null
+ }
+ {
+ tableDataObj.tableType === "OFFLINE" ?
+ <FormControl className={classes.selectFormControl}>
+ <InputLabel htmlFor="segmentPushType">Offline push type</InputLabel>
+ <Select
+ labelId="segmentPushType"
+ id="segmentPushType"
+ value={tableDataObj.segmentsConfig.segmentPushType !== "" ? tableDataObj.segmentsConfig.segmentPushType : ""}
+ onChange={(e)=> changeHandler('segmentPushType', e.target.value)}
+ >
+ <MenuItem value="APPEND">APPEND</MenuItem>
+ <MenuItem value="REFRESH">REFRESH</MenuItem>
+ </Select>
+ </FormControl> : null
+ }
+ </Grid>
+ <Grid item xs={12}>
+ <FormControl className={classes.formControl}>
+ <InputLabel htmlFor="filterFunction">Filter function</InputLabel>
+ <Input
+ id="filterFunction"
+ value={tableObj.ingestionConfig.filterConfig && tableObj.ingestionConfig.filterConfig.filterFunction || ""}
+ onChange={(e)=> changeHandler('filterFunction', e.target.value)}
+ />
+ </FormControl>
+ {
+ tableDataObj.tableIndexConfig.streamConfigs ?
+ <AddDeleteComponent
+ key = {"streamConfigs"}
+ streamConfigsObj = {{...tableDataObj.tableIndexConfig.streamConfigs}}
+ changeHandler = {changeHandler}/>
+ : null
+ }
+ <MultipleSelectComponent
+ key = {"transformConfigs"}
+ streamConfigsObj = {tableDataObj.ingestionConfig.transformConfigs || []}
+ changeHandler = {changeHandler}
+ columnName= {columnName}/>
+ </Grid>
+ </Grid>
+ );
+}
\ No newline at end of file
diff --git a/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddTableSchemaOp.tsx b/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddOfflineTableOp.tsx
similarity index 53%
rename from pinot-controller/src/main/resources/app/components/Homepage/Operations/AddTableSchemaOp.tsx
rename to pinot-controller/src/main/resources/app/components/Homepage/Operations/AddOfflineTableOp.tsx
index c87f750..74f8f4f 100644
--- a/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddTableSchemaOp.tsx
+++ b/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddOfflineTableOp.tsx
@@ -21,11 +21,18 @@ import React, { useEffect, useState } from 'react';
import { createStyles, DialogContent, Grid, makeStyles, Theme} from '@material-ui/core';
import Dialog from '../../CustomDialog';
import SimpleAccordion from '../../SimpleAccordion';
-import SchemaComponent from './SchemaComponent';
import AddTableComponent from './AddTableComponent';
import CustomCodemirror from '../../CustomCodemirror';
import PinotMethodUtils from '../../../utils/PinotMethodUtils';
import { NotificationContext } from '../../Notification/NotificationContext';
+import AddTenantComponent from './AddTenantComponent';
+import AddIngestionComponent from './AddIngestionComponent';
+import AddIndexingComponent from './AddIndexingComponent';
+import AddPartionComponent from './AddPartionComponent';
+import AddStorageComponent from './AddStorageComponent';
+import AddQueryComponent from './AddQueryComponent';
+import _ from 'lodash';
+import AddOfflineTenantComponent from './AddOfflineTenantComponent';
const useStyles = makeStyles((theme: Theme) =>
createStyles({
@@ -42,7 +49,8 @@ const useStyles = makeStyles((theme: Theme) =>
type Props = {
hideModal: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void,
- fetchData: Function
+ fetchData: Function,
+ tableType: String
};
const defaultTableObj = {
@@ -50,8 +58,7 @@ const defaultTableObj = {
"tableType": "",
"tenants": {
"broker": "DefaultTenant",
- "server": "DefaultTenant",
- "tagOverrideConfig": {}
+ "server": "DefaultTenant"
},
"segmentsConfig": {
"schemaName": "",
@@ -62,7 +69,6 @@ const defaultTableObj = {
"retentionTimeValue": null,
"segmentPushType": "APPEND",
"segmentPushFrequency": "HOURLY",
- "completionConfig": null,
"crypterClassName": null,
"peerSegmentDownloadScheme": null
},
@@ -82,22 +88,7 @@ const defaultTableObj = {
"enableDynamicStarTreeCreation": false,
"segmentPartitionConfig": null,
"columnMinMaxValueGeneratorMode": null,
- "aggregateMetrics": false,
- "nullHandlingEnabled": false,
- "streamConfigs": {
- "streamType": "kafka",
- "stream.kafka.topic.name": "",
- "stream.kafka.broker.list": "",
- "stream.kafka.consumer.type": "lowlevel",
- "stream.kafka.consumer.prop.auto.offset.reset": "smallest",
- "stream.kafka.consumer.factory.class.name": "org.apache.pinot.plugin.stream.kafka20.KafkaConsumerFactory",
- "stream.kafka.decoder.class.name": "org.apache.pinot.plugin.stream.kafka.KafkaJSONMessageDecoder",
- "stream.kafka.decoder.prop.schema.registry.rest.url": null,
- "stream.kafka.decoder.prop.schema.registry.schema.name": null,
- "realtime.segment.flush.threshold.rows": "0",
- "realtime.segment.flush.threshold.time": "24h",
- "realtime.segment.flush.segment.size": "100M"
- }
+ "nullHandlingEnabled": false
},
"metadata": {},
"ingestionConfig": {
@@ -122,37 +113,106 @@ const defaultTableObj = {
"tierConfigs": null
};
-export default function AddTableSchemaOp({
+const defaultSchemaObj = {
+ schemaName: '',
+ dimensionFieldSpecs: [],
+ metricFieldSpecs: [],
+ dateTimeFieldSpecs: []
+};
+
+let timerId = null;
+
+const tableNamekey = ["dimensionFieldSpecs","metricFieldSpecs","dateTimeFieldSpecs"];
+
+export default function AddOfflineTableOp({
hideModal,
- fetchData
+ fetchData,
+ tableType
}: Props) {
const classes = useStyles();
- const [schemaObj, setSchemaObj] = useState({schemaName:'', dateTimeFieldSpecs: []});
- const [schemaName, setSchemaName] = useState("");
const [tableObj, setTableObj] = useState(JSON.parse(JSON.stringify(defaultTableObj)));
+ const [schemaObj, setSchemaObj] = useState(JSON.parse(JSON.stringify(defaultSchemaObj)));
+ const [tableName, setTableName] = useState('');
+ const [columnName, setColumnName] = useState([]);
const {dispatch} = React.useContext(NotificationContext);
+ let isError = false;
- useEffect(() => {
- let newSchemaObj = {...schemaObj};
- newSchemaObj.schemaName = tableObj.tableName;
- setSchemaObj(newSchemaObj);
- setTimeout(()=>{setSchemaName(tableObj.tableName);},0);
- }, [tableObj])
+ useEffect(()=>{
+ if(tableName !== tableObj.tableName){
+ setTableName(tableObj.tableName);
+ clearTimeout(timerId);
+ timerId = setTimeout(()=>{
+ updateSchemaObj(tableObj.tableName);
+ }, 1000);
+ }
+ }, [tableObj]);
+
+ useEffect(()=>{
+ setTableObj({...tableObj,"tableType":tableType})
+ },[])
- const validateSchema = async () => {
- const validSchema = await PinotMethodUtils.validateSchemaAction(schemaObj);
- if(validSchema.error || typeof validSchema === 'string'){
+ const updateSchemaObj = async (tableName) => {
+ //table name is same as schema name
+ const schemaObj = await PinotMethodUtils.getSchemaData(tableName);
+ if(schemaObj.error || typeof schemaObj === 'string'){
dispatch({
type: 'error',
- message: validSchema.error || validSchema,
+ message: schemaObj.error || schemaObj,
show: true
});
- return false;
+ setSchemaObj(defaultSchemaObj)
+ } else {
+ setSchemaObj({...defaultSchemaObj, ...schemaObj});
}
- return true;
- };
+ }
+
+ const returnValue = (data,key) =>{
+ Object.keys(data).map(async (o)=>{
+ if(!_.isEmpty(data[o]) && typeof data[o] === "object"){
+ await returnValue(data[o],key);
+ }
+ else if(!_.isEmpty(data[o]) && _.isArray(data[o])){
+ data[o].map(async (obj)=>{
+ await returnValue(obj,key);
+ })
+ }else{
+ if(o === key && (data[key] === null || data[key] === "")){
+ dispatch({
+ type: 'error',
+ message: `${key} cannot be empty`,
+ show: true
+ });
+ isError = true;
+ }
+ }
+ })
+ }
+
+const checkFields = (tableObj,fields) => {
+ fields.forEach(async (o:any)=>{
+ if(tableObj[o.key] === undefined){
+ await returnValue(tableObj,o.key);
+ }else{
+ if((tableObj[o.key] === null || tableObj[o.key] === "")){
+ dispatch({
+ type: 'error',
+ message: `${o.label} cannot be empty`,
+ show: true
+ });
+ isError = true;
+ }
+ }
+ });
+ }
+
const validateTableConfig = async () => {
+ const fields = [{key:"tableName",label:"Table Name"}];
+ await checkFields(tableObj,fields);
+ if(isError){
+ isError = false;
+ return false;
+ }
const validTable = await PinotMethodUtils.validateTableAction(tableObj);
if(validTable.error || typeof validTable === 'string'){
dispatch({
@@ -166,30 +226,36 @@ export default function AddTableSchemaOp({
};
const handleSave = async () => {
- if(await validateSchema() && await validateTableConfig()){
- const schemaCreationResp = await PinotMethodUtils.saveSchemaAction(schemaObj);
- dispatch({
- type: (schemaCreationResp.error || typeof schemaCreationResp === 'string') ? 'error' : 'success',
- message: schemaCreationResp.error || schemaCreationResp.status || schemaCreationResp,
- show: true
- });
+ if(await validateTableConfig()){
const tableCreationResp = await PinotMethodUtils.saveTableAction(tableObj);
dispatch({
type: (tableCreationResp.error || typeof tableCreationResp === 'string') ? 'error' : 'success',
message: tableCreationResp.error || tableCreationResp.status || tableCreationResp,
show: true
});
- fetchData();
- hideModal(null);
+ tableCreationResp.status && fetchData();
+ tableCreationResp.status && hideModal(null);
}
};
+ useEffect(()=>{
+ let columnName = [];
+ if(!_.isEmpty(schemaObj)){
+ tableNamekey.map((o)=>{
+ schemaObj[o] && schemaObj[o].map((obj)=>{
+ columnName.push(obj.name);
+ })
+ })
+ }
+ setColumnName(columnName);
+ },[schemaObj])
+
return (
<Dialog
open={true}
handleClose={hideModal}
handleSave={handleSave}
- title="Add Schema & Table"
+ title={`Add ${tableType} Table`}
size="xl"
disableBackdropClick={true}
disableEscapeKeyDown={true}
@@ -198,24 +264,83 @@ export default function AddTableSchemaOp({
<Grid container spacing={2}>
<Grid item xs={12}>
<SimpleAccordion
- headerTitle="Add Schema"
+ headerTitle="Add Table"
showSearchBox={false}
>
- <SchemaComponent
- schemaName={schemaName}
- setSchemaObj={(o)=>{setSchemaObj(o);}}
+ <AddTableComponent
+ tableObj={tableObj}
+ setTableObj={setTableObj}
+ dateTimeFieldSpecs={schemaObj.dateTimeFieldSpecs}
+ disable={tableType !== ""}
/>
</SimpleAccordion>
</Grid>
<Grid item xs={12}>
<SimpleAccordion
- headerTitle="Add Table"
+ headerTitle="Tenants"
showSearchBox={false}
>
- <AddTableComponent
+ <AddOfflineTenantComponent
+ tableObj={{...tableObj}}
+ setTableObj={setTableObj}
+ />
+ </SimpleAccordion>
+ </Grid>
+ <Grid item xs={12}>
+ <SimpleAccordion
+ headerTitle="Ingestion"
+ showSearchBox={false}
+ >
+ <AddIngestionComponent
+ tableObj={{...tableObj}}
+ setTableObj={setTableObj}
+ columnName={columnName}
+ />
+ </SimpleAccordion>
+ </Grid>
+ <Grid item xs={12}>
+ <SimpleAccordion
+ headerTitle="Indexing & encoding"
+ showSearchBox={false}
+ >
+ <AddIndexingComponent
+ tableObj={tableObj}
+ setTableObj={setTableObj}
+ columnName={columnName}
+ />
+ </SimpleAccordion>
+ </Grid>
+ <Grid item xs={12}>
+ <SimpleAccordion
+ headerTitle="Partitioning & Routing"
+ showSearchBox={false}
+ >
+ <AddPartionComponent
+ tableObj={tableObj}
+ setTableObj={setTableObj}
+ columnName={columnName}
+ />
+ </SimpleAccordion>
+ </Grid>
+ <Grid item xs={12}>
+ <SimpleAccordion
+ headerTitle="Storage & Data retention"
+ showSearchBox={false}
+ >
+ <AddStorageComponent
+ tableObj={tableObj}
+ setTableObj={setTableObj}
+ />
+ </SimpleAccordion>
+ </Grid>
+ <Grid item xs={12}>
+ <SimpleAccordion
+ headerTitle="Query"
+ showSearchBox={false}
+ >
+ <AddQueryComponent
tableObj={tableObj}
setTableObj={setTableObj}
- dateTimeFieldSpecs={schemaObj.dateTimeFieldSpecs}
/>
</SimpleAccordion>
</Grid>
diff --git a/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddOfflineTenantComponent.tsx b/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddOfflineTenantComponent.tsx
new file mode 100644
index 0000000..4b1984d
--- /dev/null
+++ b/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddOfflineTenantComponent.tsx
@@ -0,0 +1,151 @@
+/**
+ * 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 React, { useEffect, useState } from 'react';
+import { createStyles, FormControl, Grid, Input, InputLabel, makeStyles, MenuItem, Select, TextField, Theme} from '@material-ui/core';
+import { Autocomplete } from '@material-ui/lab';
+
+const useStyles = makeStyles((theme: Theme) =>
+ createStyles({
+ formControl: {
+ margin: theme.spacing(1),
+ minWidth: 120,
+ },
+ selectFormControl: {
+ margin: theme.spacing(1),
+ width: 305
+ },
+ autoCompleteControl: {
+ '& .MuiFormControl-marginNormal': {
+ marginTop: 0
+ }
+ },
+ redColor: {
+ color: theme.palette.error.main
+ }
+ })
+);
+
+type Props = {
+ tableObj: any,
+ setTableObj: Function
+};
+
+export default function AddOfflineTenantComponent({
+ tableObj,
+ setTableObj
+}: Props) {
+ const classes = useStyles();
+
+ const [tableDataObj, setTableDataObj] = useState(tableObj);
+ const [tenantServer,setTenantServer] = useState('');
+ const [tenantBroker,setTenantBroker] = useState('');
+ const [serverOptions,setServerOptions] = useState([]);
+ const [brokerOptions,setBrokerOptions] = useState([]);
+ const [showRealtimeCompleted,setShowRealtimeCompleted] = useState(0);
+
+ const changeHandler = (fieldName, value) => {
+ let newTableObj = {...tableDataObj};
+ switch(fieldName){
+ case 'broker':
+ setTenantBroker(value);
+ newTableObj.tenants.broker = value;
+ break;
+ case 'server':
+ setTenantServer(value);
+ newTableObj.tenants.server = value;
+ break;
+ case 'tagOverrideConfig':
+ setShowRealtimeCompleted(value);
+ if(value){
+ newTableObj.tenants.tagOverrideConfig = {};
+ newTableObj.tenants.tagOverrideConfig.realtimeCompleted = `${tenantServer}_OFFLINE`;
+ }else{
+ delete newTableObj.tenants.tagOverrideConfig;
+ }
+ break;
+ case 'showRealtimeCompleted':
+ newTableObj.tenants.tagOverrideConfig.realtimeCompleted = value;
+ break;
+ }
+ setTableDataObj(newTableObj);
+ setTableObj(newTableObj);
+ };
+
+ useEffect(()=>{
+ if(!(tableDataObj.tenants.tagOverrideConfig && tableDataObj.tenants.tagOverrideConfig.realtimeCompleted)){
+ setShowRealtimeCompleted(0);
+ }
+ setTableDataObj(tableObj);
+ }, [tableObj]);
+
+
+ useEffect(()=>{
+ let serverOptions = [],
+ brokerOptions = [];
+ setServerOptions(serverOptions);
+ setBrokerOptions(brokerOptions);
+ setTenantServer(tableDataObj.tenants.server);
+ setTenantBroker(tableDataObj.tenants.broker);
+ },[])
+
+ const requiredAstrix = <span className={classes.redColor}>*</span>;
+ return (
+ <Grid container spacing={2}>
+ <Grid item xs={12}>
+ <FormControl className={classes.selectFormControl}>
+ <Autocomplete
+ key={'server'}
+ className={classes.autoCompleteControl}
+ value={tenantServer}
+ options={serverOptions}
+ onChange={(e, value)=> changeHandler('server', value ? value: '')}
+ disableClearable={true}
+ autoHighlight={true}
+ renderInput={(params) => (
+ <TextField
+ {...params}
+ label = {<>Server Tenant</>}
+ margin="normal"
+ />
+ )}
+ />
+ </FormControl>
+ <FormControl className={classes.selectFormControl}>
+ <Autocomplete
+ key={'broker'}
+ className={classes.autoCompleteControl}
+ value={tenantBroker}
+ options={brokerOptions}
+ onChange={(e, value)=> changeHandler('broker', value ? value: '')}
+ disableClearable={true}
+ autoHighlight={true}
+ renderInput={(params) => (
+ <TextField
+ {...params}
+ label = {<>Broker Tenant</>}
+ margin="normal"
+ />
+ )}
+ />
+ </FormControl>
+ </Grid>
+ </Grid>
+ );
+}
\ No newline at end of file
diff --git a/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddPartionComponent.tsx b/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddPartionComponent.tsx
new file mode 100644
index 0000000..d6d4559
--- /dev/null
+++ b/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddPartionComponent.tsx
@@ -0,0 +1,259 @@
+/**
+ * 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 React, { useEffect, useState } from 'react';
+import { createStyles, FormControl, Grid, Input, InputLabel, makeStyles, MenuItem, Select, Theme, Chip} from '@material-ui/core';
+
+const useStyles = makeStyles((theme: Theme) =>
+ createStyles({
+ formControl: {
+ margin: theme.spacing(1),
+ minWidth: 120,
+ width: 320
+ },
+ selectFormControl: {
+ margin: theme.spacing(1),
+ width: 320
+ },
+ redColor: {
+ color: theme.palette.error.main
+ }
+ })
+);
+
+type Props = {
+ tableObj: any,
+ setTableObj: Function,
+ columnName: Array<string>
+};
+
+export default function AddPartionComponent({
+ tableObj,
+ setTableObj,
+ columnName
+}: Props) {
+ const classes = useStyles();
+
+ const [tableDataObj, setTableDataObj] = useState(tableObj);
+ const [showPartition, setShowPartition] = useState(0);
+ const [showReplica, setShowReplica] = useState(0);
+ const ITEM_HEIGHT = 48;
+ const ITEM_PADDING_TOP = 8;
+ const MenuProps = {
+ PaperProps: {
+ style: {
+ maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
+ width: 250,
+ },
+ },
+ };
+
+ const [columnNameTemp,setColumnNameTemp] = useState("");
+
+ const changeHandler = (fieldName, value) => {
+ let newTableObj = {...tableDataObj};
+ switch(fieldName){
+ case 'segmentPrunerTypes':
+ // newTableObj[fieldName] = value;
+ setShowPartition(value);
+ if(value){
+ newTableObj.routing.segmentPrunerTypes = ["partition"];
+ newTableObj.tableIndexConfig.segmentPartitionConfig = {
+ columnPartitionMap : {
+ "" : {
+ functionName : "Murmur"
+ }
+ }
+ }
+ }else{
+ newTableObj.tableIndexConfig.segmentPartitionConfig = null;
+ }
+ break;
+ case 'functionName':
+ newTableObj.tableIndexConfig.segmentPartitionConfig.columnPartitionMap[columnNameTemp].functionName = value;
+ break;
+ case 'numPartitions':
+ newTableObj.tableIndexConfig.segmentPartitionConfig.columnPartitionMap[columnNameTemp].numPartitions = value;
+ break;
+ case 'instanceSelectorType':
+ if(value){
+ newTableObj.routing.instanceSelectorType = "replicaGroup"
+ }
+ else{
+ delete newTableObj.routing.instanceSelectorType;
+ }
+ setShowReplica(value);
+ break;
+ case 'numReplicaGroups':
+ if(!(newTableObj.instanceAssignmentConfigMap && newTableObj.instanceAssignmentConfigMap["OFFLINE"])){
+ newTableObj.instanceAssignmentConfigMap = {
+ ["OFFLINE"]: {
+ tagPoolConfig: {
+ tag: "DefaultTenant_OFFLINE"
+ },
+ replicaGroupPartitionConfig: {
+ replicaGroupBased: true,
+ numReplicaGroups: null,
+ numInstancesPerReplicaGroup:null
+ }
+ }
+ }
+ }
+ newTableObj.instanceAssignmentConfigMap["OFFLINE"].replicaGroupPartitionConfig.numReplicaGroups = value;
+ break;
+ case 'numInstancesPerReplicaGroup':
+ if(!(newTableObj.instanceAssignmentConfigMap && newTableObj.instanceAssignmentConfigMap["OFFLINE"])){
+ newTableObj.instanceAssignmentConfigMap = {
+ ["OFFLINE"]: {
+ tagPoolConfig: {
+ tag: "DefaultTenant_OFFLINE"
+ },
+ replicaGroupPartitionConfig: {
+ replicaGroupBased: true,
+ numReplicaGroups: null,
+ numInstancesPerReplicaGroup:null
+ }
+ }
+ }
+ }
+ newTableObj.instanceAssignmentConfigMap["OFFLINE"].replicaGroupPartitionConfig.numInstancesPerReplicaGroup = value;
+ break;
+ case 'columnName':
+ newTableObj.tableIndexConfig.segmentPartitionConfig.columnPartitionMap[value] = newTableObj.tableIndexConfig.segmentPartitionConfig.columnPartitionMap[columnNameTemp];
+ delete newTableObj.tableIndexConfig.segmentPartitionConfig.columnPartitionMap[columnNameTemp];
+ setColumnNameTemp(value);
+ break;
+ };
+ setTableDataObj(newTableObj);
+ setTableObj(newTableObj);
+ };
+
+ useEffect(()=>{
+ setTableDataObj(tableObj);
+ }, [tableObj]);
+
+ const requiredAstrix = <span className={classes.redColor}>*</span>;
+ return (
+ <Grid container spacing={2}>
+ <Grid item xs={12}>
+ <FormControl className={classes.selectFormControl}>
+ <InputLabel htmlFor="segmentPrunerTypes">Enable partitioning</InputLabel>
+ <Select
+ labelId="segmentPrunerTypes"
+ id="segmentPrunerTypes"
+ value={showPartition}
+ onChange={(e)=> changeHandler('segmentPrunerTypes', e.target.value)}
+ >
+ <MenuItem value={1}>True</MenuItem>
+ <MenuItem value={0}>False</MenuItem>
+ </Select>
+ </FormControl>
+ {
+ showPartition ?
+ <FormControl className={classes.selectFormControl}>
+ <InputLabel htmlFor="columnName">Column Name {requiredAstrix}</InputLabel>
+ <Select
+ labelId="columnName"
+ id="columnName"
+ key="columnName"
+ value={columnNameTemp}
+ onChange={(e)=> changeHandler('columnName', e.target.value)}
+ >
+ {columnName.map((val)=>{
+ return <MenuItem value={val}>{val}</MenuItem>
+ })}
+ </Select>
+ </FormControl> : null
+ }
+ {
+ showPartition ?
+ <FormControl className={classes.selectFormControl}>
+ <InputLabel htmlFor="functionName">Function Name {requiredAstrix}</InputLabel>
+ <Select
+ labelId="functionNamePartition"
+ id="functionNamePartition"
+ key="functionName"
+ value={tableDataObj.tableIndexConfig.segmentPartitionConfig.columnPartitionMap[columnNameTemp].functionName}
+ onChange={(e)=> changeHandler('functionName', e.target.value)}
+ >
+
+ <MenuItem value="Modulo">Modulo</MenuItem>
+ <MenuItem value="Murmur">Murmur</MenuItem>
+ <MenuItem value="ByteArray">ByteArray</MenuItem>
+ <MenuItem value="HashCode">HashCode</MenuItem>
+ </Select>
+ </FormControl> : null
+ }
+ {
+ showPartition ?
+ <FormControl className={classes.formControl} >
+ <InputLabel htmlFor="numPartitions">Number of partitions​ {requiredAstrix}</InputLabel>
+ <Input
+ id="numPartitions"
+ value={tableDataObj.tableIndexConfig.segmentPartitionConfig.columnPartitionMap[columnNameTemp].numPartitions}
+ onChange={(e)=> changeHandler('numPartitions', e.target.value)}
+ type="number"
+ />
+ </FormControl> : null }
+ </Grid>
+ <Grid item xs={12}>
+ <FormControl className={classes.selectFormControl}>
+ <InputLabel htmlFor="instanceSelectorType">Enable replica groups</InputLabel>
+ <Select
+ labelId="instanceSelectorType"
+ id="instanceSelectorType"
+ value={showReplica}
+ onChange={(e)=> changeHandler('instanceSelectorType', e.target.value)}
+ >
+ <MenuItem value={1}>True</MenuItem>
+ <MenuItem value={0}>False</MenuItem>
+ </Select>
+ </FormControl>
+
+ {
+ tableDataObj.routing.instanceSelectorType ?
+ <FormControl className={classes.formControl} >
+ <InputLabel htmlFor="numReplicaGroups">Number of replica groups {requiredAstrix}</InputLabel>
+ <Input
+ id="numReplicaGroups"
+ value={tableDataObj.instanceAssignmentConfigMap ? Number(tableDataObj.instanceAssignmentConfigMap["OFFLINE"].replicaGroupPartitionConfig.numReplicaGroups) : null}
+ onChange={(e)=> changeHandler('numReplicaGroups', e.target.value)}
+ type="number"
+ />
+ </FormControl>
+ : null
+ }
+
+ {
+ tableDataObj.routing.instanceSelectorType ?
+ <FormControl className={classes.formControl} >
+ <InputLabel htmlFor="numInstancesPerReplicaGroup">Number of instances per replica group​ {requiredAstrix}</InputLabel>
+ <Input
+ id="numInstancesPerReplicaGroup"
+ value={tableDataObj.instanceAssignmentConfigMap ? Number(tableDataObj.instanceAssignmentConfigMap["OFFLINE"].replicaGroupPartitionConfig.numInstancesPerReplicaGroup) : null}
+ onChange={(e)=> changeHandler('numInstancesPerReplicaGroup', e.target.value)}
+ type="number"
+ />
+ </FormControl>
+ : null
+ }
+ </Grid>
+ </Grid>
+ );
+}
\ No newline at end of file
diff --git a/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddQueryComponent.tsx b/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddQueryComponent.tsx
new file mode 100644
index 0000000..c8f08ac
--- /dev/null
+++ b/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddQueryComponent.tsx
@@ -0,0 +1,89 @@
+/**
+ * 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 React, { useEffect, useState } from 'react';
+import { createStyles, FormControl, Grid, Input, InputLabel, makeStyles, Theme} from '@material-ui/core';
+
+const useStyles = makeStyles((theme: Theme) =>
+ createStyles({
+ formControl: {
+ margin: theme.spacing(1),
+ minWidth: 120,
+ }
+ })
+);
+
+type Props = {
+ tableObj: any,
+ setTableObj: Function
+};
+
+export default function AddQueryComponent({
+ tableObj,
+ setTableObj
+}: Props) {
+ const classes = useStyles();
+
+ const [tableDataObj, setTableDataObj] = useState(tableObj);
+
+ const changeHandler = (fieldName, value) => {
+ let newTableObj = {...tableDataObj};
+ switch(fieldName){
+ case 'timeoutMs':
+ newTableObj.query[fieldName] = value;
+ break;
+ case 'maxQueriesPerSecond':
+ newTableObj.quota[fieldName] = value;
+ break;
+ }
+ setTableDataObj(newTableObj);
+ setTableObj(newTableObj);
+ };
+
+ useEffect(()=>{
+ setTableDataObj(tableObj);
+ }, [tableObj]);
+
+ return (
+ <Grid container spacing={2}>
+ <Grid item xs={12}>
+ <FormControl className={classes.formControl} >
+ <InputLabel htmlFor="timeoutMs">Query Timeout Ms</InputLabel>
+ <Input
+ id="timeoutMs"
+ key="timeoutMs"
+ value={tableDataObj.query.timeoutMs || ""}
+ onChange={(e)=> changeHandler('timeoutMs', e.target.value)}
+ type="number"
+ />
+ </FormControl>
+ <FormControl className={classes.formControl} >
+ <InputLabel htmlFor="maxQueriesPerSecond">Queries Per Second</InputLabel>
+ <Input
+ id="maxQueriesPerSecond"
+ key="maxQueriesPerSecond"
+ value={tableDataObj.quota.maxQueriesPerSecond || ""}
+ onChange={(e)=> changeHandler('maxQueriesPerSecond', e.target.value)}
+ type="number"
+ />
+ </FormControl>
+ </Grid>
+ </Grid>
+ );
+}
\ No newline at end of file
diff --git a/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddRealTimeIngestionComponent.tsx b/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddRealTimeIngestionComponent.tsx
new file mode 100644
index 0000000..d9fab60
--- /dev/null
+++ b/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddRealTimeIngestionComponent.tsx
@@ -0,0 +1,167 @@
+/**
+ * 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 React, { useEffect, useState } from 'react';
+import { createStyles, FormControl, Grid, Input, InputLabel, makeStyles, MenuItem, Select, Theme} from '@material-ui/core';
+import AddDeleteComponent from './AddDeleteComponent';
+import MultipleSelectComponent from './MultipleSelectComponent';
+import _ from 'lodash';
+
+const useStyles = makeStyles((theme: Theme) =>
+ createStyles({
+ formControl: {
+ margin: theme.spacing(1),
+ minWidth: 120,
+ },
+ selectFormControl: {
+ margin: theme.spacing(1),
+ width: 220
+ },
+ })
+);
+
+type Props = {
+ tableObj: any,
+ setTableObj: Function,
+ columnName: Array<string>
+};
+
+export default function AddRealTimeIngestionComponent({
+ tableObj,
+ setTableObj,
+ columnName
+}: Props) {
+ const classes = useStyles();
+ const [newSize,setNewSize] = useState([0,1])
+ const [tableDataObj, setTableDataObj] = useState(tableObj);
+
+ const changeHandler = (fieldName, value) => {
+ let newTableObj = {...tableDataObj};
+ switch(fieldName){
+ case 'filterConfig':
+ newTableObj.ingestionConfig.filterConfig[fieldName] = value;
+ break;
+ case 'segmentPushFrequency':
+ newTableObj.segmentsConfig[fieldName] = value;
+ break;
+ case 'segmentPushType':
+ newTableObj.segmentsConfig[fieldName] = value;
+ break;
+ case 'streamConfigs':
+ newTableObj.tableIndexConfig.streamConfigs = {...value};
+ break;
+ case 'filterFunction':
+ if(!newTableObj.ingestionConfig.filterConfig){
+ newTableObj.ingestionConfig.filterConfig = {};
+ }
+ newTableObj.ingestionConfig.filterConfig.filterFunction = value;
+ break;
+ case 'transformConfigs':
+ tableDataObj.ingestionConfig.transformConfigs = value;
+ };
+ setTableDataObj(newTableObj);
+ setTableObj(newTableObj);
+ };
+
+ useEffect(()=>{
+ let newTableObj = {...tableObj};
+ if(newTableObj.tableType === "REALTIME" && !newTableObj.tableIndexConfig.streamConfigs && _.isEmpty(newTableObj.tableIndexConfig.streamConfigs) ){
+ newTableObj.tableIndexConfig.streamConfigs =
+ {
+ "streamType": "kafka",
+ "stream.kafka.topic.name": "",
+ "stream.kafka.broker.list": "",
+ "stream.kafka.consumer.type": "lowlevel",
+ "stream.kafka.consumer.prop.auto.offset.reset": "smallest",
+ "stream.kafka.consumer.factory.class.name":"org.apache.pinot.plugin.stream.kafka20.KafkaConsumerFactory",
+ "stream.kafka.decoder.class.name":"org.apache.pinot.plugin.stream.kafka.KafkaJSONMessageDecoder",
+ "realtime.segment.flush.threshold.rows": "0",
+ "realtime.segment.flush.threshold.time": "24h",
+ "realtime.segment.flush.segment.size": "100M"
+ }
+ setTableObj(newTableObj);
+ }else if(newTableObj.tableType !== "REALTIME" && newTableObj.tableIndexConfig.streamConfigs){
+ newTableObj.tableIndexConfig.streamConfigs = null;
+ setTableObj(newTableObj);
+ }
+ setTableDataObj(newTableObj);
+ }, [tableObj]);
+
+ useEffect(()=>{
+ setNewSize(newSize);
+ },[newSize])
+
+ return (
+ <Grid container spacing={2}>
+ <Grid item xs={12}>
+ {
+ tableDataObj.tableType === "OFFLINE" ?
+ <FormControl className={classes.selectFormControl}>
+ <InputLabel htmlFor="segmentPushFrequency">Offline push frequency</InputLabel>
+ <Select
+ labelId="segmentPushFrequency"
+ id="segmentPushFrequency"
+ value={tableDataObj.segmentsConfig.segmentPushFrequency !== "" ? tableDataObj.segmentsConfig.segmentPushFrequency : ""}
+ onChange={(e)=> changeHandler('segmentPushFrequency', e.target.value)}
+ >
+ <MenuItem value="HOURLY">HOURLY</MenuItem>
+ <MenuItem value="DAILY">DAILY</MenuItem>
+ </Select>
+ </FormControl> : null
+ }
+ {
+ tableDataObj.tableType === "OFFLINE" ?
+ <FormControl className={classes.selectFormControl}>
+ <InputLabel htmlFor="segmentPushType">Offline push type</InputLabel>
+ <Select
+ labelId="segmentPushType"
+ id="segmentPushType"
+ value={tableDataObj.segmentsConfig.segmentPushType !== "" ? tableDataObj.segmentsConfig.segmentPushType : ""}
+ onChange={(e)=> changeHandler('segmentPushType', e.target.value)}
+ >
+ <MenuItem value="APPEND">APPEND</MenuItem>
+ <MenuItem value="REFRESH">REFRESH</MenuItem>
+ </Select>
+ </FormControl> : null
+ }
+ <FormControl className={classes.formControl}>
+ <InputLabel htmlFor="filterFunction">Filter function</InputLabel>
+ <Input
+ id="filterFunction"
+ value={tableObj.ingestionConfig.filterConfig && tableObj.ingestionConfig.filterConfig.filterFunction || ""}
+ onChange={(e)=> changeHandler('filterFunction', e.target.value)}
+ />
+ </FormControl>
+ <MultipleSelectComponent
+ key = {"transformConfigs"}
+ streamConfigsObj = {tableDataObj.ingestionConfig.transformConfigs || []}
+ changeHandler = {changeHandler}
+ columnName= {columnName}/>
+ {
+ tableDataObj.tableIndexConfig.streamConfigs ?
+ <AddDeleteComponent
+ key = {"streamConfigs"}
+ streamConfigsObj = {{...tableDataObj.tableIndexConfig.streamConfigs}}
+ changeHandler = {changeHandler}/>
+ : null
+ }
+ </Grid>
+ </Grid>
+ );
+}
\ No newline at end of file
diff --git a/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddRealTimePartionComponent.tsx b/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddRealTimePartionComponent.tsx
new file mode 100644
index 0000000..c5126e0
--- /dev/null
+++ b/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddRealTimePartionComponent.tsx
@@ -0,0 +1,230 @@
+/**
+ * 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 React, { useEffect, useState } from 'react';
+import { createStyles, FormControl, Grid, Input, InputLabel, makeStyles, MenuItem, Select, Theme, Chip} from '@material-ui/core';
+
+const useStyles = makeStyles((theme: Theme) =>
+ createStyles({
+ formControl: {
+ margin: theme.spacing(1),
+ minWidth: 120,
+ width: 300
+ },
+ selectFormControl: {
+ margin: theme.spacing(1),
+ width: 300
+ },
+ redColor: {
+ color: theme.palette.error.main
+ }
+ })
+);
+
+type Props = {
+ tableObj: any,
+ setTableObj: Function,
+ columnName: Array<string>
+};
+
+export default function AddRealTimePartionComponent({
+ tableObj,
+ setTableObj,
+ columnName
+}: Props) {
+ const classes = useStyles();
+
+ const [tableDataObj, setTableDataObj] = useState(tableObj);
+ const [showPartition, setShowPartition] = useState(0);
+ const [showReplica, setShowReplica] = useState(0);
+ const ITEM_HEIGHT = 48;
+ const ITEM_PADDING_TOP = 8;
+ const MenuProps = {
+ PaperProps: {
+ style: {
+ maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
+ width: 250,
+ },
+ },
+ };
+ const [columnNameTemp,setColumnNameTemp] = useState("");
+
+ const changeHandler = (fieldName, value) => {
+ let newTableObj = {...tableDataObj};
+ switch(fieldName){
+ case 'segmentPrunerTypes':
+ // newTableObj[fieldName] = value;
+ setShowPartition(value);
+ if(value){
+ newTableObj.routing.segmentPrunerTypes = ["partition"];
+ newTableObj.tableIndexConfig.segmentPartitionConfig = {
+ columnPartitionMap : {
+ "" : {
+ functionName : "Murmur"
+ }
+ }
+ }
+ }else{
+ newTableObj.tableIndexConfig.segmentPartitionConfig = null;
+ }
+ break;
+ case 'functionName':
+ newTableObj.tableIndexConfig.segmentPartitionConfig.columnPartitionMap[columnNameTemp].functionName = value;
+ break;
+ case 'numPartitions':
+ newTableObj.tableIndexConfig.segmentPartitionConfig.columnPartitionMap[columnNameTemp].numPartitions = value;
+ break;
+ case 'instanceSelectorType':
+ if(value){
+ newTableObj.routing.instanceSelectorType = "replicaGroup"
+ }
+ else{
+ delete newTableObj.routing.instanceSelectorType;
+ }
+ setShowReplica(value);
+ break;
+ case 'numReplicaGroups':
+ if(!(newTableObj.instanceAssignmentConfigMap && newTableObj.instanceAssignmentConfigMap["OFFLINE"])){
+ newTableObj.instanceAssignmentConfigMap = {
+ ["OFFLINE"]: {
+ tagPoolConfig: {
+ tag: "DefaultTenant_OFFLINE"
+ },
+ replicaGroupPartitionConfig: {
+ replicaGroupBased: true,
+ numReplicaGroups: null,
+ numInstancesPerReplicaGroup:null
+ }
+ }
+ }
+ }
+ newTableObj.instanceAssignmentConfigMap["OFFLINE"].replicaGroupPartitionConfig.numReplicaGroups = value;
+ break;
+ case 'numInstancesPerReplicaGroup':
+ if(!(newTableObj.instanceAssignmentConfigMap && newTableObj.instanceAssignmentConfigMap["OFFLINE"])){
+ newTableObj.instanceAssignmentConfigMap = {
+ ["OFFLINE"]: {
+ tagPoolConfig: {
+ tag: "DefaultTenant_OFFLINE"
+ },
+ replicaGroupPartitionConfig: {
+ replicaGroupBased: true,
+ numReplicaGroups: null,
+ numInstancesPerReplicaGroup:null
+ }
+ }
+ }
+ }
+ newTableObj.instanceAssignmentConfigMap["OFFLINE"].replicaGroupPartitionConfig.numInstancesPerReplicaGroup = value;
+ break;
+ case 'columnName':
+ newTableObj.tableIndexConfig.segmentPartitionConfig.columnPartitionMap[value] = newTableObj.tableIndexConfig.segmentPartitionConfig.columnPartitionMap[columnNameTemp];
+ delete newTableObj.tableIndexConfig.segmentPartitionConfig.columnPartitionMap[columnNameTemp];
+ setColumnNameTemp(value);
+ break;
+ };
+ setTableDataObj(newTableObj);
+ setTableObj(newTableObj);
+ };
+
+ useEffect(()=>{
+ setTableDataObj(tableObj);
+ }, [tableObj]);
+
+ const requiredAstrix = <span className={classes.redColor}>*</span>;
+
+ return (
+ <Grid container spacing={2}>
+ <Grid item xs={12}>
+ <FormControl className={classes.selectFormControl}>
+ <InputLabel htmlFor="segmentPrunerTypes">Enable partitioning</InputLabel>
+ <Select
+ labelId="segmentPrunerTypes"
+ id="segmentPrunerTypes"
+ value={showPartition}
+ onChange={(e)=> changeHandler('segmentPrunerTypes', e.target.value)}
+ >
+ <MenuItem value={1}>True</MenuItem>
+ <MenuItem value={0}>False</MenuItem>
+ </Select>
+ </FormControl>
+ {
+ showPartition ?
+ <FormControl className={classes.selectFormControl}>
+ <InputLabel htmlFor="columnName">Column Name {requiredAstrix}</InputLabel>
+ <Select
+ labelId="columnName"
+ id="columnName"
+ key="columnName"
+ value={columnNameTemp}
+ onChange={(e)=> changeHandler('columnName', e.target.value)}
+ >
+ {columnName.map((val)=>{
+ return <MenuItem value={val}>{val}</MenuItem>
+ })}
+ </Select>
+ </FormControl> : null
+ }
+ {
+ showPartition ?
+ <FormControl className={classes.selectFormControl}>
+ <InputLabel htmlFor="functionName">Function Name {requiredAstrix}</InputLabel>
+ <Select
+ labelId="functionNamePartition"
+ id="functionNamePartition"
+ key="functionName"
+ value={tableDataObj.tableIndexConfig.segmentPartitionConfig.columnPartitionMap[columnNameTemp].functionName}
+ onChange={(e)=> changeHandler('functionName', e.target.value)}
+ >
+ <MenuItem value="Modulo">Modulo</MenuItem>
+ <MenuItem value="Murmur">Murmur</MenuItem>
+ <MenuItem value="ByteArray">ByteArray</MenuItem>
+ <MenuItem value="HashCode">HashCode</MenuItem>
+ </Select>
+ </FormControl> : null
+ }
+ {
+ showPartition ?
+ <FormControl className={classes.formControl} >
+ <InputLabel htmlFor="numPartitions">Number of partitions​ {requiredAstrix}</InputLabel>
+ <Input
+ id="numPartitions"
+ value={tableDataObj.tableIndexConfig.segmentPartitionConfig.columnPartitionMap[columnNameTemp].numPartitions}
+ onChange={(e)=> changeHandler('numPartitions', e.target.value)}
+ type="number"
+ />
+ </FormControl> : null }
+ </Grid>
+ <Grid item xs={12}>
+ <FormControl className={classes.selectFormControl}>
+ <InputLabel htmlFor="instanceSelectorType">Enable replica groups</InputLabel>
+ <Select
+ labelId="instanceSelectorType"
+ id="instanceSelectorType"
+ value={showReplica}
+ onChange={(e)=> changeHandler('instanceSelectorType', e.target.value)}
+ >
+ <MenuItem value={1}>True</MenuItem>
+ <MenuItem value={0}>False</MenuItem>
+ </Select>
+ </FormControl>
+ </Grid>
+ </Grid>
+ );
+}
\ No newline at end of file
diff --git a/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddTableComponent.tsx b/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddRealtimeTableComponent.tsx
similarity index 97%
copy from pinot-controller/src/main/resources/app/components/Homepage/Operations/AddTableComponent.tsx
copy to pinot-controller/src/main/resources/app/components/Homepage/Operations/AddRealtimeTableComponent.tsx
index 2867df3..0efca0c 100644
--- a/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddTableComponent.tsx
+++ b/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddRealtimeTableComponent.tsx
@@ -46,12 +46,14 @@ type Props = {
tableObj: any,
setTableObj: Function,
dateTimeFieldSpecs: Array<any>
+ disable:boolean
};
-export default function AddTableComponent({
+export default function AddRealtimeTableComponent({
tableObj,
setTableObj,
- dateTimeFieldSpecs
+ dateTimeFieldSpecs,
+ disable
}: Props) {
const classes = useStyles();
@@ -122,7 +124,7 @@ export default function AddTableComponent({
onChange={(e)=> changeHandler('tableName', e.target.value)}
/>
</FormControl>
-
+
<FormControl className={classes.selectFormControl}>
<InputLabel htmlFor="tableType">Table Type {requiredAstrix}</InputLabel>
<Select
@@ -130,6 +132,7 @@ export default function AddTableComponent({
id="tableType"
value={tableDataObj.tableType}
onChange={(e)=> changeHandler('tableType', e.target.value)}
+ disabled={disable}
>
<MenuItem value="OFFLINE">OFFLINE</MenuItem>
<MenuItem value="REALTIME">REALTIME</MenuItem>
diff --git a/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddTableOp.tsx b/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddRealtimeTableOp.tsx
similarity index 61%
rename from pinot-controller/src/main/resources/app/components/Homepage/Operations/AddTableOp.tsx
rename to pinot-controller/src/main/resources/app/components/Homepage/Operations/AddRealtimeTableOp.tsx
index 8b4bac1..08a95c7 100644
--- a/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddTableOp.tsx
+++ b/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddRealtimeTableOp.tsx
@@ -25,6 +25,15 @@ import AddTableComponent from './AddTableComponent';
import CustomCodemirror from '../../CustomCodemirror';
import PinotMethodUtils from '../../../utils/PinotMethodUtils';
import { NotificationContext } from '../../Notification/NotificationContext';
+import AddTenantComponent from './AddTenantComponent';
+import AddIngestionComponent from './AddIngestionComponent';
+import AddIndexingComponent from './AddIndexingComponent';
+import AddPartionComponent from './AddPartionComponent';
+import AddStorageComponent from './AddStorageComponent';
+import AddQueryComponent from './AddQueryComponent';
+import _ from 'lodash';
+import AddRealTimeIngestionComponent from './AddRealTimeIngestionComponent';
+import AddRealTimePartionComponent from './AddRealTimePartionComponent';
const useStyles = makeStyles((theme: Theme) =>
createStyles({
@@ -41,7 +50,8 @@ const useStyles = makeStyles((theme: Theme) =>
type Props = {
hideModal: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void,
- fetchData: Function
+ fetchData: Function,
+ tableType: String
};
const defaultTableObj = {
@@ -59,8 +69,6 @@ const defaultTableObj = {
"replicasPerPartition": "1",
"retentionTimeUnit": null,
"retentionTimeValue": null,
- "segmentPushType": "APPEND",
- "segmentPushFrequency": "HOURLY",
"completionConfig": null,
"crypterClassName": null,
"peerSegmentDownloadScheme": null
@@ -91,8 +99,6 @@ const defaultTableObj = {
"stream.kafka.consumer.prop.auto.offset.reset": "smallest",
"stream.kafka.consumer.factory.class.name": "org.apache.pinot.plugin.stream.kafka20.KafkaConsumerFactory",
"stream.kafka.decoder.class.name": "org.apache.pinot.plugin.stream.kafka.KafkaJSONMessageDecoder",
- "stream.kafka.decoder.prop.schema.registry.rest.url": null,
- "stream.kafka.decoder.prop.schema.registry.schema.name": null,
"realtime.segment.flush.threshold.rows": "0",
"realtime.segment.flush.threshold.time": "24h",
"realtime.segment.flush.segment.size": "100M"
@@ -112,7 +118,6 @@ const defaultTableObj = {
"segmentPrunerTypes": null,
"instanceSelectorType": null
},
- "instanceAssignmentConfigMap": null,
"query": {
"timeoutMs": null
},
@@ -130,15 +135,20 @@ const defaultSchemaObj = {
let timerId = null;
-export default function AddTableOp({
+const tableNamekey = ["dimensionFieldSpecs","metricFieldSpecs","dateTimeFieldSpecs"];
+
+export default function AddRealtimeTableOp({
hideModal,
- fetchData
+ fetchData,
+ tableType
}: Props) {
const classes = useStyles();
const [tableObj, setTableObj] = useState(JSON.parse(JSON.stringify(defaultTableObj)));
const [schemaObj, setSchemaObj] = useState(JSON.parse(JSON.stringify(defaultSchemaObj)));
const [tableName, setTableName] = useState('');
+ const [columnName, setColumnName] = useState([]);
const {dispatch} = React.useContext(NotificationContext);
+ let isError = false;
useEffect(()=>{
if(tableName !== tableObj.tableName){
@@ -150,6 +160,10 @@ export default function AddTableOp({
}
}, [tableObj]);
+ useEffect(()=>{
+ setTableObj({...tableObj,"tableType":tableType})
+ },[])
+
const updateSchemaObj = async (tableName) => {
//table name is same as schema name
const schemaObj = await PinotMethodUtils.getSchemaData(tableName);
@@ -164,8 +178,53 @@ export default function AddTableOp({
setSchemaObj({...defaultSchemaObj, ...schemaObj});
}
}
-
+
+ const returnValue = (data,key) =>{
+ Object.keys(data).map(async (o)=>{
+ if(!_.isEmpty(data[o]) && typeof data[o] === "object"){
+ await returnValue(data[o],key);
+ }
+ else if(!_.isEmpty(data[o]) && _.isArray(data[o])){
+ data[o].map(async (obj)=>{
+ await returnValue(obj,key);
+ })
+ }else{
+ if(o === key && (data[key] === null || data[key] === "")){
+ dispatch({
+ type: 'error',
+ message: `${key} cannot be empty`,
+ show: true
+ });
+ isError = true;
+ }
+ }
+ })
+ }
+
+const checkFields = (tableObj,fields) => {
+ fields.forEach(async (o:any)=>{
+ if(tableObj[o.key] === undefined){
+ await returnValue(tableObj,o.key);
+ }else{
+ if((tableObj[o.key] === null || tableObj[o.key] === "")){
+ dispatch({
+ type: 'error',
+ message: `${o.label} cannot be empty`,
+ show: true
+ });
+ isError = true;
+ }
+ }
+ });
+ }
+
const validateTableConfig = async () => {
+ const fields = [{key:"tableName",label:"Table Name"},{key:"tableType",label:"Table Type"},{key:"stream.kafka.broker.list",label:"stream.kafka.broker.list"},{key:"stream.kafka.topic.name",label:"stream.kafka.topic.name"},{key:"stream.kafka.consumer.type",label:"stream.kafka.consumer.type"},{key:"stream.kafka.decoder.class.name",label:"stream.kafka.decoder.class.name"}];
+ await checkFields(tableObj,fields);
+ if(isError){
+ isError = false;
+ return false;
+ }
const validTable = await PinotMethodUtils.validateTableAction(tableObj);
if(validTable.error || typeof validTable === 'string'){
dispatch({
@@ -191,12 +250,24 @@ export default function AddTableOp({
}
};
+ useEffect(()=>{
+ let columnName = [];
+ if(!_.isEmpty(schemaObj)){
+ tableNamekey.map((o)=>{
+ schemaObj[o] && schemaObj[o].map((obj)=>{
+ columnName.push(obj.name);
+ })
+ })
+ }
+ setColumnName(columnName);
+ },[schemaObj])
+
return (
<Dialog
open={true}
handleClose={hideModal}
handleSave={handleSave}
- title="Add Table"
+ title={`Add ${tableType} Table`}
size="xl"
disableBackdropClick={true}
disableEscapeKeyDown={true}
@@ -212,6 +283,76 @@ export default function AddTableOp({
tableObj={tableObj}
setTableObj={setTableObj}
dateTimeFieldSpecs={schemaObj.dateTimeFieldSpecs}
+ disable={tableType !== ""}
+ />
+ </SimpleAccordion>
+ </Grid>
+ <Grid item xs={12}>
+ <SimpleAccordion
+ headerTitle="Tenants"
+ showSearchBox={false}
+ >
+ <AddTenantComponent
+ tableObj={{...tableObj}}
+ setTableObj={setTableObj}
+ />
+ </SimpleAccordion>
+ </Grid>
+ <Grid item xs={12}>
+ <SimpleAccordion
+ headerTitle="Ingestion"
+ showSearchBox={false}
+ >
+ <AddRealTimeIngestionComponent
+ tableObj={{...tableObj}}
+ setTableObj={setTableObj}
+ columnName={columnName}
+ />
+ </SimpleAccordion>
+ </Grid>
+ <Grid item xs={12}>
+ <SimpleAccordion
+ headerTitle="Indexing & encoding"
+ showSearchBox={false}
+ >
+ <AddIndexingComponent
+ tableObj={tableObj}
+ setTableObj={setTableObj}
+ columnName={columnName}
+ />
+ </SimpleAccordion>
+ </Grid>
+ <Grid item xs={12}>
+ <SimpleAccordion
+ headerTitle="Partitioning & Routing"
+ showSearchBox={false}
+ >
+ <AddRealTimePartionComponent
+ tableObj={tableObj}
+ setTableObj={setTableObj}
+ columnName={columnName}
+ />
+ </SimpleAccordion>
+ </Grid>
+ <Grid item xs={12}>
+ <SimpleAccordion
+ headerTitle="Storage & Data retention"
+ showSearchBox={false}
+ >
+ <AddStorageComponent
+ tableObj={tableObj}
+ setTableObj={setTableObj}
+ />
+ </SimpleAccordion>
+ </Grid>
+ <Grid item xs={12}>
+ <SimpleAccordion
+ headerTitle="Query"
+ showSearchBox={false}
+ >
+ <AddQueryComponent
+ tableObj={tableObj}
+ setTableObj={setTableObj}
/>
</SimpleAccordion>
</Grid>
diff --git a/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddSchemaOp.tsx b/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddSchemaOp.tsx
new file mode 100644
index 0000000..72011cd
--- /dev/null
+++ b/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddSchemaOp.tsx
@@ -0,0 +1,203 @@
+/**
+ * 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 React, { useEffect, useState } from 'react';
+import { createStyles, DialogContent, Grid, makeStyles, Theme} from '@material-ui/core';
+import Dialog from '../../CustomDialog';
+import SimpleAccordion from '../../SimpleAccordion';
+import SchemaComponent from './SchemaComponent';
+import CustomCodemirror from '../../CustomCodemirror';
+import PinotMethodUtils from '../../../utils/PinotMethodUtils';
+import { NotificationContext } from '../../Notification/NotificationContext';
+import _ from 'lodash';
+import SchemaNameComponent from './SchemaNameComponent';
+import CustomizedTables from '../../Table';
+
+const useStyles = makeStyles((theme: Theme) =>
+ createStyles({
+ sqlDiv: {
+ border: '1px #BDCCD9 solid',
+ borderRadius: 4,
+ marginBottom: '20px',
+ },
+ queryOutput: {
+ '& .CodeMirror': { height: '532px !important' },
+ },
+ })
+);
+
+type Props = {
+ hideModal: (event: React.MouseEvent<HTMLElement, MouseEvent>) => void,
+ fetchData: Function
+};
+
+export default function AddSchemaOp({
+ hideModal,
+ fetchData
+}: Props) {
+ const classes = useStyles();
+ const [schemaObj, setSchemaObj] = useState({schemaName:'', dateTimeFieldSpecs: []});
+ const {dispatch} = React.useContext(NotificationContext);
+ let isError = false
+
+ const returnValue = (data,key) =>{
+ Object.keys(data).map(async (o)=>{
+ if(!_.isEmpty(data[o]) && typeof data[o] === "object"){
+ await returnValue(data[o],key);
+ }
+ else if(!_.isEmpty(data[o]) && _.isArray(data[o])){
+ data[o].map(async (obj)=>{
+ await returnValue(obj,key);
+ })
+ }else{
+ if(o === key && (data[key] === null || data[key] === "")){
+ dispatch({
+ type: 'error',
+ message: `${key} cannot be empty`,
+ show: true
+ });
+ isError = true;
+ }
+ }
+ })
+ }
+
+ const checkFields = (tableObj,fields) => {
+ fields.forEach(async (o:any)=>{
+ if(tableObj[o.key] === undefined){
+ await returnValue(tableObj,o.key);
+ }else{
+ if((tableObj[o.key] === null || tableObj[o.key] === "")){
+ dispatch({
+ type: 'error',
+ message: `${o.label} cannot be empty`,
+ show: true
+ });
+ isError = true;
+ }
+ }
+ });
+ }
+
+ const isObjEmpty = () =>{
+ const types = ["dimensionFieldSpecs","metricFieldSpecs","dateTimeFieldSpecs"];
+ let notEmpty = true;
+ types.map((t)=>{
+ if(schemaObj[t].length)
+ {
+ notEmpty = false
+ }
+ })
+ return notEmpty;
+ }
+
+ const validateSchema = async () => {
+ const validSchema = await PinotMethodUtils.validateSchemaAction(schemaObj);
+ if(validSchema.error || typeof validSchema === 'string'){
+ dispatch({
+ type: 'error',
+ message: validSchema.error || validSchema,
+ show: true
+ });
+ return false;
+ }
+ return true;
+ };
+
+ const handleSave = async () => {
+ const fields = [{key:"schemaName",label:"schema Name"},{key:"name",label:"Column Name"},{key:"dataType",label:"Data Type"}];
+ await checkFields(schemaObj,fields);
+ if(isError){
+ isError = false;
+ return false;
+ }
+ if(!isObjEmpty()){
+ if(await validateSchema()){
+ const schemaCreationResp = await PinotMethodUtils.saveSchemaAction(schemaObj);
+ dispatch({
+ type: (schemaCreationResp.error || typeof schemaCreationResp === 'string') ? 'error' : 'success',
+ message: schemaCreationResp.error || schemaCreationResp.status || schemaCreationResp,
+ show: true
+ });
+ if(!schemaCreationResp.error && typeof schemaCreationResp !== 'string'){
+ fetchData();
+ hideModal(null);
+ }
+ }
+ }else{
+ dispatch({
+ type: 'error',
+ message: "Please Enter atleast one Type",
+ show: true
+ });
+ }
+ };
+
+ return (
+ <Dialog
+ open={true}
+ handleClose={hideModal}
+ handleSave={handleSave}
+ title="Add Schema"
+ size="xl"
+ disableBackdropClick={true}
+ disableEscapeKeyDown={true}
+ >
+ <DialogContent>
+ <Grid container spacing={2}>
+ <Grid item xs={12}>
+ <SimpleAccordion
+ headerTitle="Add Schema"
+ showSearchBox={false}
+ >
+ <SchemaComponent
+ schemaObj={schemaObj}
+ schemaName={schemaObj.schemaName}
+ setSchemaObj={(o)=>{setSchemaObj(o);}}
+ />
+ </SimpleAccordion>
+ </Grid>
+ <Grid item xs={6}>
+ <div className={classes.sqlDiv}>
+ <SimpleAccordion
+ headerTitle="Schema Config (Read Only)"
+ showSearchBox={false}
+ >
+ <CustomCodemirror
+ customClass={classes.queryOutput}
+ data={schemaObj}
+ isEditable={false}
+ returnCodemirrorValue={(newValue)=>{
+ try{
+ const jsonObj = JSON.parse(newValue);
+ if(jsonObj){
+ jsonObj.segmentsConfig.replicasPerPartition = jsonObj.segmentsConfig.replication;
+ setSchemaObj(jsonObj);
+ }
+ }catch(e){}
+ }}
+ />
+ </SimpleAccordion>
+ </div>
+ </Grid>
+ </Grid>
+ </DialogContent>
+ </Dialog>
+ );
+}
\ No newline at end of file
diff --git a/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddStorageComponent.tsx b/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddStorageComponent.tsx
new file mode 100644
index 0000000..37bf2b4
--- /dev/null
+++ b/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddStorageComponent.tsx
@@ -0,0 +1,117 @@
+/**
+ * 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 React, { useEffect, useState } from 'react';
+import { createStyles, FormControl, Grid, Input, InputLabel, makeStyles, MenuItem, Select, Theme} from '@material-ui/core';
+
+const useStyles = makeStyles((theme: Theme) =>
+ createStyles({
+ formControl: {
+ margin: theme.spacing(1),
+ minWidth: 120,
+ },
+ selectFormControl: {
+ margin: theme.spacing(1),
+ width: 170
+ }
+ })
+);
+
+type Props = {
+ tableObj: any,
+ setTableObj: Function
+};
+
+export default function AddStorageComponent({
+ tableObj,
+ setTableObj
+}: Props) {
+ const classes = useStyles();
+
+ const [tableDataObj, setTableDataObj] = useState(tableObj);
+
+ const changeHandler = (fieldName, value) => {
+ let newTableObj = {...tableDataObj};
+ switch(fieldName){
+ case 'retentionTimeUnit':
+ newTableObj.segmentsConfig.retentionTimeUnit = value;
+ break;
+ case 'retentionTimeValue':
+ newTableObj.segmentsConfig.retentionTimeValue = value;
+ break;
+ case 'maxQueriesPerSecond':
+ newTableObj.quota.storage = value;
+ break;
+ };
+ setTableDataObj(newTableObj);
+ setTableObj(newTableObj);
+ };
+
+ useEffect(()=>{
+ setTableDataObj(tableObj);
+ }, [tableObj]);
+
+ return (
+ <Grid container spacing={2}>
+ <Grid item xs={12}>
+
+ <FormControl className={classes.formControl} >
+ <InputLabel htmlFor="retentionTimeValue">Retention Value</InputLabel>
+ <Input
+ id="retentionTimeValue"
+ key="retentionTimeValue"
+ value={tableDataObj.segmentsConfig.retentionTimeValue || ""}
+ onChange={(e)=>
+ changeHandler('retentionTimeValue', e.target.value)
+ }
+ type="number"
+ />
+ </FormControl>
+
+ <FormControl className={classes.selectFormControl}>
+ <InputLabel htmlFor="retentionTimeUnit">Retention unit</InputLabel>
+ <Select
+ labelId="retentionTimeUnit"
+ id="retentionTimeUnit"
+ key="retentionTimeUnit"
+ value={tableDataObj.segmentsConfig.retentionTimeUnit && tableDataObj.segmentsConfig.retentionTimeUnit !== "" ? tableDataObj.segmentsConfig.retentionTimeUnit : ''}
+ onChange={(e)=>
+ changeHandler('retentionTimeUnit', e.target.value)
+ }
+ >
+ <MenuItem value="HOURS">HOURS</MenuItem>
+ <MenuItem value="DAYS">DAYS</MenuItem>
+ </Select>
+ </FormControl>
+
+ <FormControl className={classes.formControl} >
+ <InputLabel htmlFor="maxQueriesPerSecond">Storage Quota</InputLabel>
+ <Input
+ id="maxQueriesPerSecond"
+ key="maxQueriesPerSecond"
+ value={tableDataObj.quota.storage || ""}
+ onChange={(e)=>
+ changeHandler('maxQueriesPerSecond', e.target.value)
+ }
+ />
+ </FormControl>
+ </Grid>
+ </Grid>
+ );
+}
\ No newline at end of file
diff --git a/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddTableComponent.tsx b/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddTableComponent.tsx
index 2867df3..13de6c9 100644
--- a/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddTableComponent.tsx
+++ b/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddTableComponent.tsx
@@ -46,12 +46,14 @@ type Props = {
tableObj: any,
setTableObj: Function,
dateTimeFieldSpecs: Array<any>
+ disable:boolean
};
export default function AddTableComponent({
tableObj,
setTableObj,
- dateTimeFieldSpecs
+ dateTimeFieldSpecs,
+ disable
}: Props) {
const classes = useStyles();
@@ -130,12 +132,12 @@ export default function AddTableComponent({
id="tableType"
value={tableDataObj.tableType}
onChange={(e)=> changeHandler('tableType', e.target.value)}
+ disabled={disable}
>
<MenuItem value="OFFLINE">OFFLINE</MenuItem>
<MenuItem value="REALTIME">REALTIME</MenuItem>
</Select>
</FormControl>
-
<FormControl className={classes.selectFormControl}>
<Autocomplete
className={classes.autoCompleteControl}
@@ -155,7 +157,7 @@ export default function AddTableComponent({
</FormControl>
<FormControl className={classes.selectFormControl}>
- <InputLabel htmlFor="replication">Replication {requiredAstrix}</InputLabel>
+ <InputLabel htmlFor="replication">Replication</InputLabel>
<Select
labelId="replication"
id="replication"
diff --git a/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddTenantComponent.tsx b/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddTenantComponent.tsx
new file mode 100644
index 0000000..f92bddc
--- /dev/null
+++ b/pinot-controller/src/main/resources/app/components/Homepage/Operations/AddTenantComponent.tsx
@@ -0,0 +1,172 @@
+/**
+ * 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 React, { useEffect, useState } from 'react';
+import { createStyles, FormControl, Grid, Input, InputLabel, makeStyles, MenuItem, Select, TextField, Theme} from '@material-ui/core';
+import { Autocomplete } from '@material-ui/lab';
+
+const useStyles = makeStyles((theme: Theme) =>
+ createStyles({
+ formControl: {
+ margin: theme.spacing(1),
+ minWidth: 120,
+ },
+ selectFormControl: {
+ margin: theme.spacing(1),
+ width: 305
+ },
+ autoCompleteControl: {
+ '& .MuiFormControl-marginNormal': {
+ marginTop: 0
+ }
+ },
+ redColor: {
+ color: theme.palette.error.main
+ }
+ })
+);
+
+type Props = {
+ tableObj: any,
+ setTableObj: Function
+};
+
+export default function AddTenantComponent({
+ tableObj,
+ setTableObj
+}: Props) {
+ const classes = useStyles();
+
+ const [tableDataObj, setTableDataObj] = useState(tableObj);
+ const [tenantServer,setTenantServer] = useState('');
+ const [tenantBroker,setTenantBroker] = useState('');
+ const [serverOptions,setServerOptions] = useState([]);
+ const [brokerOptions,setBrokerOptions] = useState([]);
+ const [showRealtimeCompleted,setShowRealtimeCompleted] = useState(0);
+
+ const changeHandler = (fieldName, value) => {
+ let newTableObj = {...tableDataObj};
+ switch(fieldName){
+ case 'broker':
+ setTenantBroker(value);
+ newTableObj.tenants.broker = value;
+ break;
+ case 'server':
+ setTenantServer(value);
+ newTableObj.tenants.server = value;
+ break;
+ case 'tagOverrideConfig':
+ setShowRealtimeCompleted(value);
+ if(value){
+ newTableObj.tenants.tagOverrideConfig = {};
+ newTableObj.tenants.tagOverrideConfig.realtimeCompleted = `${tenantServer}_OFFLINE`;
+ }else{
+ delete newTableObj.tenants.tagOverrideConfig;
+ }
+ break;
+ case 'showRealtimeCompleted':
+ newTableObj.tenants.tagOverrideConfig.realtimeCompleted = value;
+ break;
+ }
+ setTableDataObj(newTableObj);
+ setTableObj(newTableObj);
+ };
+
+ useEffect(()=>{
+ if(!(tableDataObj.tenants.tagOverrideConfig && tableDataObj.tenants.tagOverrideConfig.realtimeCompleted)){
+ setShowRealtimeCompleted(0);
+ }
+ setTableDataObj(tableObj);
+ }, [tableObj]);
+
+
+ useEffect(()=>{
+ let serverOptions = [],
+ brokerOptions = [];
+ setServerOptions(serverOptions);
+ setBrokerOptions(brokerOptions);
+ setTenantServer(tableDataObj.tenants.server);
+ setTenantBroker(tableDataObj.tenants.broker);
+ },[])
+
+ const requiredAstrix = <span className={classes.redColor}>*</span>;
+ return (
+ <Grid container spacing={2}>
+ <Grid item xs={12}>
+ <FormControl className={classes.selectFormControl}>
+ <Autocomplete
+ key={'server'}
+ className={classes.autoCompleteControl}
+ value={tenantServer}
+ options={serverOptions}
+ onChange={(e, value)=> changeHandler('server', value ? value: '')}
+ disableClearable={true}
+ autoHighlight={true}
+ renderInput={(params) => (
+ <TextField
+ {...params}
+ label = {<>Server Tenant</>}
+ margin="normal"
+ />
+ )}
+ />
+ </FormControl>
+ <FormControl className={classes.selectFormControl}>
+ <Autocomplete
+ key={'broker'}
+ className={classes.autoCompleteControl}
+ value={tenantBroker}
+ options={brokerOptions}
+ onChange={(e, value)=> changeHandler('broker', value ? value: '')}
+ disableClearable={true}
+ autoHighlight={true}
+ renderInput={(params) => (
+ <TextField
+ {...params}
+ label = {<>Broker Tenant</>}
+ margin="normal"
+ />
+ )}
+ />
+ </FormControl>
+ <FormControl className={classes.selectFormControl}>
+ <InputLabel htmlFor="tagOverrideConfig">Relocate realtime completed segments?</InputLabel>
+ <Select
+ labelId="tagOverrideConfig"
+ id="tagOverrideConfig"
+ value={showRealtimeCompleted}
+ onChange={(e)=> changeHandler('tagOverrideConfig', e.target.value)}
+ >
+ <MenuItem value={1}>True</MenuItem>
+ <MenuItem value={0}>False</MenuItem>
+ </Select>
+ </FormControl>
+ { showRealtimeCompleted ?
+ <FormControl className={classes.formControl}>
+ <InputLabel htmlFor="realtimeCompleted">Relocate to tag</InputLabel>
+ <Input
+ id="realtimeCompleted"
+ value={tableDataObj.tenants.tagOverrideConfig.realtimeCompleted}
+ onChange={(e)=> changeHandler('showRealtimeCompleted', e.target.value)}
+ />
+ </FormControl> : null}
+ </Grid>
+ </Grid>
+ );
+}
\ No newline at end of file
diff --git a/pinot-controller/src/main/resources/app/components/Homepage/Operations/MultiIndexingComponent.tsx b/pinot-controller/src/main/resources/app/components/Homepage/Operations/MultiIndexingComponent.tsx
new file mode 100644
index 0000000..7b3f5f5
--- /dev/null
+++ b/pinot-controller/src/main/resources/app/components/Homepage/Operations/MultiIndexingComponent.tsx
@@ -0,0 +1,344 @@
+/**
+ * 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 React, { useEffect, useState } from 'react';
+import { createStyles, FormControl, Grid, Input, InputLabel, makeStyles, MenuItem, Select, TextField, Theme, IconButton, Fab, Button, Chip} from '@material-ui/core';
+import AddCircleIcon from '@material-ui/icons/AddCircle';
+import ClearIcon from '@material-ui/icons/Clear';
+
+const useStyles = makeStyles((theme: Theme) =>
+ createStyles({
+ formControl: {
+ margin: theme.spacing(1),
+ width: '100%'
+ },
+ selectFormControl: {
+ margin: theme.spacing(1),
+ width: 170
+ },
+ deleteIcon:{
+ marginTop:15,
+ color: theme.palette.error.main
+ },
+ autoCompleteControl: {
+ '& .MuiFormControl-marginNormal': {
+ marginTop: 0
+ }
+ },
+ redColor: {
+ color: theme.palette.error.main
+ }
+ })
+);
+
+type Props = {
+ changeHandler: any,
+ streamConfigsObj: Object,
+ columnName:Array<string>,
+ textDataObj: Array<TextObj>
+};
+
+type TextObj = {
+ name:string,
+ encodingType:string,
+ indexType:string
+}
+
+export default function MultiIndexingComponent({
+ changeHandler,
+ streamConfigsObj,
+ columnName,
+ textDataObj
+}: Props) {
+ const classes = useStyles();
+ const ITEM_HEIGHT = 48;
+ const ITEM_PADDING_TOP = 8;
+ const MenuProps = {
+ PaperProps: {
+ style: {
+ maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
+ width: 250,
+ },
+ },
+ };
+
+ const [jsonUpdated,setJsonUpdated] = useState(false);
+
+ const updateFieldForColumnName = (tempStreamConfigObj,values,Field,majorField) =>{
+ values.map((v)=>{
+ let valChanged = false;
+ tempStreamConfigObj.map((o)=>{
+ if(v === o.columnName){
+ if("Encoding" === majorField)
+ {
+ o[majorField] = Field;
+ }else{
+ o[majorField].push(Field);
+ }
+ valChanged = true;
+ }
+ })
+ if(!valChanged){
+ tempStreamConfigObj.push({
+ columnName : v,
+ Indexing : [],
+ Encoding: "",
+ [majorField] : "Encoding" === majorField ? Field : [Field]
+ })
+ }
+ })
+ return tempStreamConfigObj;
+ }
+
+ const convertInputToData = (input,isJsonUpdated) =>{
+ let tempStreamConfigObj = [];
+ Object.keys(input).map((o)=>{
+ switch(o){
+ case "invertedIndexColumns":
+ tempStreamConfigObj = updateFieldForColumnName(tempStreamConfigObj,input[o],"Inverted","Indexing");
+ break;
+ case "rangeIndexColumns":
+ tempStreamConfigObj = updateFieldForColumnName(tempStreamConfigObj,input[o],"Range","Indexing");
+ break;
+ case "sortedColumn":
+ tempStreamConfigObj = updateFieldForColumnName(tempStreamConfigObj,input[o],"Sorted","Indexing");
+ break;
+ case "bloomFilterColumns":
+ tempStreamConfigObj = updateFieldForColumnName(tempStreamConfigObj,input[o],"bloomFilter","Indexing");
+ break;
+ case "noDictionaryColumns":
+ tempStreamConfigObj = updateFieldForColumnName(tempStreamConfigObj,input[o],"None","Encoding");
+ break;
+ case "DictionaryColumns":
+ tempStreamConfigObj = updateFieldForColumnName(tempStreamConfigObj,input[o],"Dictionary","Encoding");
+ break;
+ case "onHeapDictionaryColumns":
+ tempStreamConfigObj = updateFieldForColumnName(tempStreamConfigObj,input[o],"heapDictionary","Encoding");
+ break;
+ case "varLengthDictionaryColumns":
+ tempStreamConfigObj = updateFieldForColumnName(tempStreamConfigObj,input[o],"varDictionary","Encoding");
+ break;
+ }
+ })
+ if(textDataObj && textDataObj.length){
+ textDataObj.map((o)=>{
+ tempStreamConfigObj = updateFieldForColumnName(tempStreamConfigObj,[o.name],"Text","Indexing");
+ })
+ }
+ return tempStreamConfigObj;
+ }
+
+ const [streamConfigObj, setStreamConfigObj] = useState(convertInputToData(streamConfigsObj,false));
+
+ const addButtonClick = ()=>{
+ let data = {
+ columnName : "",
+ Indexing : [],
+ Encoding: ""
+ };
+ let tempStreamConfigObj = [...streamConfigObj,data]
+ setStreamConfigObj(tempStreamConfigObj);
+ // changeHandler('tableIndexConfig',convertDataToInput(tempStreamConfigObj));
+ }
+
+ const convertDataToInput = async (input) => {
+ let outputData = {
+ invertedIndexColumns: [],
+ rangeIndexColumns : [],
+ sortedColumn : [],
+ noDictionaryColumns : [],
+ DictionaryColumns: [],
+ onHeapDictionaryColumns : [],
+ varLengthDictionaryColumns : [],
+ bloomFilterColumns : []
+ };
+ let fieldConfigList= [];
+
+ input.map((i)=>{
+ i.Indexing.map((o)=>{
+ switch(o){
+ case "Sorted":
+ outputData.sortedColumn.push(i.columnName);
+ break;
+ case "Inverted":
+ outputData.invertedIndexColumns.push(i.columnName);
+ break;
+ case "Range":
+ outputData.rangeIndexColumns.push(i.columnName);
+ break;
+ case "bloomFilter":
+ outputData.bloomFilterColumns.push(i.columnName);
+ break;
+ case "Text":
+ fieldConfigList.push({"name":i.columnName, "encodingType":"RAW", "indexType":"TEXT"})
+ break;
+ }
+ })
+ switch(i.Encoding){
+ case "None":
+ outputData.noDictionaryColumns.push(i.columnName);
+ break;
+ case "Dictionary":
+ outputData.DictionaryColumns.push(i.columnName);
+ break;
+ case "heapDictionary":
+ outputData.onHeapDictionaryColumns.push(i.columnName);
+ break;
+ case "varDictionary":
+ outputData.varLengthDictionaryColumns.push(i.columnName);
+ break;
+ }
+ });
+ return {
+ tableIndexConfig:outputData,
+ fieldConfigList
+ };
+ }
+
+ const keyChange = (input,index, value) =>{
+ let tempStreamConfigObj = [...streamConfigObj];
+ tempStreamConfigObj[index][input] = value;
+ setStreamConfigObj(tempStreamConfigObj);
+ }
+
+ const updateJson = async () =>{
+ changeHandler('tableIndexConfig',await convertDataToInput(streamConfigObj));
+ }
+
+ const deleteClick = async (ind) =>{
+ let tempStreamConfigObj = [...streamConfigObj];
+ tempStreamConfigObj.splice(ind,1);
+ setStreamConfigObj(tempStreamConfigObj);
+ changeHandler('tableIndexConfig',await convertDataToInput(tempStreamConfigObj));
+ }
+
+ useEffect(() => {
+ if(jsonUpdated){
+ setJsonUpdated(false);
+ }
+ else{
+ let value = convertInputToData(streamConfigsObj,true);
+ if(value.length >= streamConfigObj.length){
+ setStreamConfigObj(value);
+ }
+ setJsonUpdated(true);
+ }
+ }, [streamConfigsObj]);
+
+ return (
+ <Grid container spacing={2}>
+ {
+ streamConfigObj && streamConfigObj.map((o,i)=>{
+ return(
+ <Grid item xs={6}>
+ <div className="box-border">
+ <Grid container spacing={2}>
+ <Grid item xs={3}>
+ <FormControl className={classes.formControl}>
+ <InputLabel htmlFor={o.columnName}>Column Name</InputLabel>
+ <Select
+ labelId={o.columnName}
+ id={o.columnName}
+ value={o.columnName}
+ key={i+"columnName"}
+ onChange={(e)=> keyChange("columnName",i,e.target.value)}
+ onBlur={updateJson}
+ >
+ {columnName.map((val)=>{
+ return <MenuItem value={val}>{val}</MenuItem>
+ })}
+ </Select>
+ </FormControl>
+ </Grid>
+ <Grid item xs={4}>
+ <FormControl className={classes.formControl}>
+ <InputLabel htmlFor={o.Encoding}>Encoding</InputLabel>
+ <Select
+ labelId={o.Encoding}
+ id={o.Encoding}
+ key={i+"Encoding"}
+ value={o.Encoding}
+ onChange={(e)=> keyChange("Encoding",i,e.target.value)}
+ onBlur={updateJson}
+ >
+ <MenuItem value="None">None</MenuItem>
+ <MenuItem value="Dictionary">Dictionary</MenuItem>
+ <MenuItem value="varDictionary">Variable Length Dictionary</MenuItem>
+ <MenuItem value="heapDictionary">On Heap Dictionary</MenuItem>
+ </Select>
+ </FormControl>
+ </Grid>
+ <Grid item xs={4}>
+ <FormControl className={classes.formControl}>
+ <InputLabel htmlFor={i+"keymulti"}>Indexing</InputLabel>
+ <Select
+ labelId={i+"keymulti"}
+ id={i+"keymulti"}
+ key={i+"Indexing"}
+ multiple
+ value={o.Indexing || []}
+ onChange={(e)=> keyChange("Indexing",i,e.target.value)}
+ input={<Input id="select-multiple-chip" />}
+ renderValue={(selected) => (
+ <div className={"chips"}>
+ {(selected as string[]).map((value) => (
+ <Chip key={value} label={value} className={"chip"} />
+ ))}
+ </div>
+ )}
+ MenuProps={MenuProps}
+ onBlur={updateJson}>
+ <MenuItem value="None">None</MenuItem>
+ <MenuItem value="Sorted">Sorted</MenuItem>
+ <MenuItem value="Inverted">Inverted</MenuItem>
+ <MenuItem value="Range">Range</MenuItem>
+ <MenuItem value="Text">Text</MenuItem>
+ <MenuItem value="bloomFilter">Bloom filter</MenuItem>
+ </Select>
+ </FormControl>
+ </Grid>
+ <Grid item xs={1}>
+ <FormControl>
+ <IconButton aria-label="delete" className={classes.deleteIcon} onClick={()=>{
+ deleteClick(i)}}>
+ <ClearIcon />
+ </IconButton>
+ </FormControl>
+ </Grid>
+ </Grid>
+ </div>
+ </Grid>)
+ })
+ }
+ <Grid item xs={3}>
+ <FormControl className={classes.formControl}>
+ <Button
+ aria-label="plus"
+ variant="outlined"
+ color="primary"
+ onClick={addButtonClick}
+ startIcon={(<AddCircleIcon />)}
+ >
+ Add new Field
+ </Button>
+ </FormControl>
+ </Grid>
+ </Grid>
+ );
+}
\ No newline at end of file
diff --git a/pinot-controller/src/main/resources/app/components/Homepage/Operations/MultiMetricComponent.tsx b/pinot-controller/src/main/resources/app/components/Homepage/Operations/MultiMetricComponent.tsx
new file mode 100644
index 0000000..b73c943
--- /dev/null
+++ b/pinot-controller/src/main/resources/app/components/Homepage/Operations/MultiMetricComponent.tsx
@@ -0,0 +1,182 @@
+/**
+ * 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 React, { useEffect, useState } from 'react';
+import { createStyles, FormControl, Grid, Input, InputLabel, makeStyles, MenuItem, Select, TextField, Theme, IconButton, Fab, Button, Chip} from '@material-ui/core';
+import AddIcon from '@material-ui/icons/Add';
+import AddCircleIcon from '@material-ui/icons/AddCircle';
+import ClearIcon from '@material-ui/icons/Clear';
+import DeleteIcon from '@material-ui/icons/Delete';
+
+const useStyles = makeStyles((theme: Theme) =>
+ createStyles({
+ formControl: {
+ margin: theme.spacing(1),
+ width: '100%'
+ },
+ deleteIcon:{
+ marginTop:15,
+ color: theme.palette.error.main
+ },
+ selectFormControl: {
+ margin: theme.spacing(1),
+ width: 170
+ },
+ autoCompleteControl: {
+ '& .MuiFormControl-marginNormal': {
+ marginTop: 0
+ }
+ },
+ redColor: {
+ color: theme.palette.error.main
+ }
+ })
+);
+
+type Props = {
+ changeHandler: any,
+ streamConfigsObj: Array<any>
+};
+
+export default function MultiMetricComponent({
+ changeHandler,
+ streamConfigsObj
+}: Props) {
+ const classes = useStyles();
+ const [streamConfigObj, setStreamConfigObj] = useState(streamConfigsObj);
+ const ITEM_HEIGHT = 48;
+ const ITEM_PADDING_TOP = 8;
+ const MenuProps = {
+ PaperProps: {
+ style: {
+ maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
+ width: 250,
+ },
+ },
+ };
+
+ const addButtonClick = ()=>{
+ let data = {columnName:"",transformFunction:""};
+ let tempStreamConfigObj = [...streamConfigObj,data];
+ setStreamConfigObj(tempStreamConfigObj);
+ changeHandler('transformConfigs',tempStreamConfigObj);
+ }
+
+ const inputChange = (input,index, value) =>{
+ let tempStreamConfigObj = [...streamConfigObj]
+ tempStreamConfigObj[index][input] = value;
+ setStreamConfigObj(tempStreamConfigObj);
+ }
+
+ const updateJson = () =>{
+ changeHandler('transformConfigs',streamConfigObj);
+ }
+
+ const deleteClick = (index) =>{
+ let tempStreamConfigObj = {...streamConfigObj};
+ tempStreamConfigObj.splice(index,1);
+ changeHandler('transformConfigs',tempStreamConfigObj)
+ }
+
+ useEffect(() => {
+ setStreamConfigObj(streamConfigsObj);
+ }, [streamConfigsObj]);
+
+
+ return (
+ <Grid container spacing={2}>
+ <h3 className="accordion-subtitle">Transform functions</h3>
+ {
+ streamConfigObj.map((o,i)=>{
+ return(
+ <Grid item xs={4}>
+ <Grid container spacing={2}>
+ <Grid item xs={5}>
+ <FormControl className={classes.formControl}>
+ <InputLabel htmlFor={o.columnName}>Column Name</InputLabel>
+ <Input
+ id={o.columnName}
+ value={o.columnName}
+ key={i+"keyval"}
+ onChange={(e)=> inputChange("columnName",i,e.target.value)}
+ onBlur={updateJson}
+ />
+ </FormControl>
+ </Grid>
+ <Grid item xs={5}>
+ <FormControl className={classes.formControl}>
+ <InputLabel htmlFor={i+"keymulti"}>Transform Function</InputLabel>
+ {/* <Select
+ labelId={o.transformFunction}
+ id={o.transformFunction}
+ value={i+"valuemulti"}
+ onChange={(e)=> inputChange("transformFunction",i,e.target.value)}
+ onBlur={updateJson}
+ > */}
+ <Select
+ id={i+"keymulti"}
+ labelId={i+"keymulti"}
+ key={i+"keymulti"}
+ multiple
+ value={o.transformFunction || []}
+ onChange={(e)=> inputChange("transformFunction",i,e.target.value)}
+ input={<Input id="select-multiple-chip" />}
+ renderValue={(selected) => (
+ <div className={"chips"}>
+ {(selected as string[]).map((value) => (
+ <Chip key={value} label={value} className={"chip"} />
+ ))}
+ </div>
+ )}
+ MenuProps={MenuProps}
+ >
+ <MenuItem value="HOURLY">HOURLY</MenuItem>
+ <MenuItem value="DAILY">DAILY</MenuItem>
+ </Select>
+ </FormControl>
+
+ </Grid>
+ <Grid item xs={2}>
+ <FormControl>
+ <IconButton aria-label="delete" className = {classes.deleteIcon} onClick={()=>{
+ deleteClick(i)}}>
+ <ClearIcon />
+ </IconButton>
+ </FormControl>
+ </Grid>
+ </Grid>
+ </Grid>)
+ })
+ }
+ <Grid item xs={3}>
+ <FormControl className={classes.formControl}>
+ <Button
+ aria-label="plus"
+ variant="outlined"
+ color="primary"
+ onClick={addButtonClick}
+ startIcon={(<AddCircleIcon />)}
+ >
+ Add new Field
+ </Button>
+ </FormControl>
+ </Grid>
+ </Grid>
+ );
+}
\ No newline at end of file
diff --git a/pinot-controller/src/main/resources/app/components/Homepage/Operations/MultipleSelectComponent.tsx b/pinot-controller/src/main/resources/app/components/Homepage/Operations/MultipleSelectComponent.tsx
new file mode 100644
index 0000000..04fdb4d
--- /dev/null
+++ b/pinot-controller/src/main/resources/app/components/Homepage/Operations/MultipleSelectComponent.tsx
@@ -0,0 +1,185 @@
+/**
+ * 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 React, { useEffect, useState } from 'react';
+import { createStyles, FormControl, Grid, Input, InputLabel, makeStyles, MenuItem, Select, TextField, Theme, IconButton, Fab, Button, Chip} from '@material-ui/core';
+import AddIcon from '@material-ui/icons/Add';
+import AddCircleIcon from '@material-ui/icons/AddCircle';
+import ClearIcon from '@material-ui/icons/Clear';
+import DeleteIcon from '@material-ui/icons/Delete';
+
+const useStyles = makeStyles((theme: Theme) =>
+ createStyles({
+ formControl: {
+ margin: theme.spacing(1),
+ width: '100%'
+ },
+ deleteIcon:{
+ marginTop:15,
+ color: theme.palette.error.main
+ },
+ selectFormControl: {
+ margin: theme.spacing(1),
+ width: 170
+ },
+ autoCompleteControl: {
+ '& .MuiFormControl-marginNormal': {
+ marginTop: 0
+ }
+ }
+ })
+);
+
+type Props = {
+ changeHandler: any,
+ streamConfigsObj: Array<any>,
+ columnName: Array<string>
+};
+
+export default function AddDeleteComponent({
+ changeHandler,
+ streamConfigsObj,
+ columnName
+}: Props) {
+ const classes = useStyles();
+ const [streamConfigObj, setStreamConfigObj] = useState(streamConfigsObj);
+ const ITEM_HEIGHT = 48;
+ const ITEM_PADDING_TOP = 8;
+ const MenuProps = {
+ PaperProps: {
+ style: {
+ maxHeight: ITEM_HEIGHT * 4.5 + ITEM_PADDING_TOP,
+ width: 250,
+ },
+ },
+ };
+
+ const addButtonClick = ()=>{
+ let data = {columnName:"",transformFunction:""};
+ let tempStreamConfigObj = [...streamConfigObj,data];
+ setStreamConfigObj(tempStreamConfigObj);
+ changeHandler('transformConfigs',tempStreamConfigObj);
+ }
+
+ const inputChange = (input,index, value) =>{
+ let tempStreamConfigObj = [...streamConfigObj]
+ tempStreamConfigObj[index][input] = value;
+ setStreamConfigObj(tempStreamConfigObj);
+ }
+
+ const updateJson = () =>{
+ changeHandler('transformConfigs',streamConfigObj);
+ }
+
+ const deleteClick = (index) =>{
+ let tempStreamConfigObj = [...streamConfigObj];
+ tempStreamConfigObj.splice(index,1);
+ changeHandler('transformConfigs',tempStreamConfigObj)
+ }
+
+ useEffect(() => {
+ setStreamConfigObj(streamConfigsObj);
+ }, [streamConfigsObj]);
+
+
+ return (
+ <Grid container spacing={2}>
+ <h3 className="accordion-subtitle">Transform functions</h3>
+ {
+ streamConfigObj.map((o,i)=>{
+ return(
+ <Grid item xs={4}>
+ <div className="box-border">
+ <Grid container spacing={2}>
+ <Grid item xs={5}>
+ <FormControl className={classes.formControl}>
+ <InputLabel htmlFor={o.columnName}>Column Name</InputLabel>
+ <Select
+ labelId={o.columnName }
+ id={o.columnName}
+ value={o.columnName}
+ key = {i+"valuemulti"}
+ onChange={(e)=> inputChange("columnName",i,e.target.value)}
+ onBlur={updateJson}
+ >
+ {/* <Select
+ labelId={o.columnName}
+ id={o.columnName}
+ key={i+"keymulti"}
+ multiple
+ value={o.columnName || []}
+ onChange={(e)=> inputChange("columnName",i,e.target.value)}
+ input={<Input id="select-multiple-chip" />}
+ renderValue={(selected) => (
+ <div className={"chips"}>
+ {(selected as string[]).map((value) => (
+ <Chip key={value} label={value} className={"chip"} />
+ ))}
+ </div>
+ )}
+ MenuProps={MenuProps}
+ > */}
+ {columnName.map((val)=>{
+ return <MenuItem value={val}>{val}</MenuItem>
+ })}
+ </Select>
+ </FormControl>
+
+ </Grid>
+ <Grid item xs={5}>
+ <FormControl className={classes.formControl}>
+ <InputLabel htmlFor={o.transformFunction}>Transform Function</InputLabel>
+ <Input
+ id={o.transformFunction}
+ value={o.transformFunction}
+ key={i+"keyval"}
+ onChange={(e)=> inputChange("transformFunction",i,e.target.value)}
+ onBlur={updateJson}
+ />
+ </FormControl>
+ </Grid>
+ <Grid item xs={2}>
+ <FormControl>
+ <IconButton aria-label="delete" className = {classes.deleteIcon} onClick={()=>{
+ deleteClick(i)}}>
+ <ClearIcon />
+ </IconButton>
+ </FormControl>
+ </Grid>
+ </Grid>
+ </div>
+ </Grid>)
+ })
+ }
+ <Grid item xs={3}>
+ <FormControl className={classes.formControl}>
+ <Button
+ aria-label="plus"
+ variant="outlined"
+ color="primary"
+ onClick={addButtonClick}
+ startIcon={(<AddCircleIcon />)}
+ >
+ Add new Field
+ </Button>
+ </FormControl>
+ </Grid>
+ </Grid>
+ );
+}
\ No newline at end of file
diff --git a/pinot-controller/src/main/resources/app/components/Homepage/Operations/SchemaComponent.tsx b/pinot-controller/src/main/resources/app/components/Homepage/Operations/SchemaComponent.tsx
index c1d2c8e..69ccb5b 100644
--- a/pinot-controller/src/main/resources/app/components/Homepage/Operations/SchemaComponent.tsx
+++ b/pinot-controller/src/main/resources/app/components/Homepage/Operations/SchemaComponent.tsx
@@ -22,6 +22,8 @@ import { createStyles, FormControl, Grid, IconButton, Input, InputLabel, makeSty
import AddIcon from '@material-ui/icons/Add';
import ClearIcon from '@material-ui/icons/Clear';
import { Autocomplete } from '@material-ui/lab';
+import { debug } from 'webpack';
+import _ from 'lodash';
const useStyles = makeStyles((theme: Theme) =>
createStyles({
@@ -50,7 +52,7 @@ const useStyles = makeStyles((theme: Theme) =>
},
selectFormControl: {
margin: theme.spacing(1),
- width: 170
+ width: 220
},
MVFFormControl: {
margin: theme.spacing(1),
@@ -72,12 +74,14 @@ const useStyles = makeStyles((theme: Theme) =>
type Props = {
schemaName: string,
- setSchemaObj: Function
+ setSchemaObj: Function,
+ schemaObj? : any
};
export default function SchemaComponent({
schemaName = '',
- setSchemaObj
+ setSchemaObj,
+ schemaObj,
}: Props) {
const classes = useStyles();
const defaultColumnObj = {
@@ -165,9 +169,25 @@ export default function SchemaComponent({
setColumnList(newColumnList);
}
+ const schemaNameChange = (schemaName) =>{
+ let newSchemaObj = {...schemaObj};
+ newSchemaObj.schemaName = schemaName;
+ setSchemaObj(newSchemaObj);
+ }
+
const requiredAstrix = <span className={classes.redColor}>*</span>;
return (
<Grid container className={classes.rootContainer}>
+ <Grid item xs={12} key={"schemaName"}>
+ <FormControl className={classes.formControl}>
+ <InputLabel htmlFor="schemaName">Schema Name {requiredAstrix}</InputLabel>
+ <Input
+ id="schemaName"
+ value={schemaName}
+ onChange={(e)=> schemaNameChange(e.target.value)}
+ />
+ </FormControl>
+ </Grid>
{columnList.map((columnObj, index)=>{
return(
<Grid item xs={12} key={index} className={classes.gridItem}>
@@ -206,7 +226,7 @@ export default function SchemaComponent({
{defaultDataTypeOptions[columnObj.type || 'dimension'].map((dataType, i)=>(<MenuItem key={i} value={dataType}>{dataType}</MenuItem>))}
</Select>
</FormControl>
-
+{/*
<FormControl className={classes.formControl}>
<InputLabel htmlFor="defaultNullValue">Default Null Value</InputLabel>
<Input
@@ -214,8 +234,7 @@ export default function SchemaComponent({
value={columnObj.defaultNullValue}
onChange={(e)=> changeHandler(index, 'defaultNullValue', e.target.value)}
/>
- </FormControl>
-
+ </FormControl> */}
{columnObj.type === 'dimension' &&
<FormControl className={classes.MVFFormControl}>
<InputLabel htmlFor="multiValue">Multi Value Field</InputLabel>
@@ -230,9 +249,8 @@ export default function SchemaComponent({
</Select>
</FormControl>
}
-
- <br/>
-
+ {columnObj.type !== 'datetime' &&
+ <>
<FormControl className={classes.formControl}>
<InputLabel htmlFor="maxLength">Max Value Length</InputLabel>
<Input
@@ -253,10 +271,12 @@ export default function SchemaComponent({
)}
/>
</FormControl>
- </div>
+ </>
+ }
+ <br/>
{columnObj.type === 'datetime' &&
- <div className={classes.dateTimeDiv}>
+ <div>
<FormControl className={classes.formControl}>
<InputLabel htmlFor="timeUnit">Time Unit {requiredAstrix}</InputLabel>
<Select
@@ -296,7 +316,6 @@ export default function SchemaComponent({
/>
</FormControl>
}
- <br/>
<FormControl className={classes.formControl}>
<InputLabel htmlFor="granularityInterval">Granularity Interval {requiredAstrix}</InputLabel>
<Input
@@ -323,6 +342,7 @@ export default function SchemaComponent({
</FormControl>
</div>
}
+ </div>
<div className={classes.iconDiv}>
<FormControl>
<IconButton
diff --git a/pinot-controller/src/main/resources/app/components/Homepage/Operations/SchemaNameComponent.tsx b/pinot-controller/src/main/resources/app/components/Homepage/Operations/SchemaNameComponent.tsx
new file mode 100644
index 0000000..54fe9ee
--- /dev/null
+++ b/pinot-controller/src/main/resources/app/components/Homepage/Operations/SchemaNameComponent.tsx
@@ -0,0 +1,104 @@
+/**
+ * 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 React, { useEffect, useState } from 'react';
+import { createStyles, FormControl, Grid, Input, InputLabel, makeStyles, Theme} from '@material-ui/core';
+
+const useStyles = makeStyles((theme: Theme) =>
+ createStyles({
+ rootContainer:{
+ padding: '0 15px 15px'
+ },
+ gridItem:{
+ display: 'flex',
+ alignItems: 'center',
+ marginTop: '15px',
+ padding: '5px',
+ border: '1px #ccc solid',
+ borderRadius: 5
+ },
+ formControl: {
+ margin: theme.spacing(1),
+ minWidth: 120,
+ },
+ selectFormControl: {
+ margin: theme.spacing(1),
+ width: 300
+ },
+ MVFFormControl: {
+ margin: theme.spacing(1),
+ width: 126
+ },
+ autoCompleteControl: {
+ '& .MuiFormControl-marginNormal': {
+ marginTop: 0
+ }
+ },
+ greenColor: {
+ color: theme.palette.success.main
+ },
+ redColor: {
+ color: theme.palette.error.main
+ },
+ })
+);
+
+type Props = {
+ setSchemaObj: Function,
+ schemaName: string,
+ schemaObj:any
+};
+
+export default function SchemaNameComponent({
+ setSchemaObj,
+ schemaName,
+ schemaObj
+}: Props) {
+ const classes = useStyles();
+ const [schemaObjTemp,setSchemaObjTemp] = useState(schemaObj);
+
+ const changeHandler = (value) =>{
+ let newSchemaObj = {...schemaObjTemp};
+ newSchemaObj.schemaName = value;
+ setSchemaObj(newSchemaObj);
+ }
+
+
+ useEffect(()=>{
+ let newSchemaObj = {...schemaObjTemp};
+ newSchemaObj.schemaName = schemaName;
+ setSchemaObjTemp(newSchemaObj);
+ },[schemaName])
+
+ const requiredAstrix = <span className={classes.redColor}>*</span>;
+ return (
+ <Grid container className={classes.rootContainer}>
+ <Grid item xs={12} key={"schemaName"}>
+ <FormControl className={classes.formControl}>
+ <InputLabel htmlFor="schemaName">Schema Name {requiredAstrix}</InputLabel>
+ <Input
+ id="schemaName"
+ value={schemaObjTemp.schemaName}
+ onChange={(e)=> changeHandler(e.target.value)}
+ />
+ </FormControl>
+ </Grid>
+ </Grid>
+ );
+}
\ No newline at end of file
diff --git a/pinot-controller/src/main/resources/app/interfaces/types.d.ts b/pinot-controller/src/main/resources/app/interfaces/types.d.ts
index 2acbc7e..6d66719 100644
--- a/pinot-controller/src/main/resources/app/interfaces/types.d.ts
+++ b/pinot-controller/src/main/resources/app/interfaces/types.d.ts
@@ -24,6 +24,14 @@ declare module 'Models' {
error?: string;
};
+ type SchemaDetails = {
+ schemaName: string,
+ totalColumns: number,
+ dimensions: number,
+ metrics: number,
+ dateTime: number
+ };
+
export type Tenants = {
SERVER_TENANTS: Array<string>;
BROKER_TENANTS: Array<string>;
diff --git a/pinot-controller/src/main/resources/app/pages/SchemaPageDetails.tsx b/pinot-controller/src/main/resources/app/pages/SchemaPageDetails.tsx
new file mode 100644
index 0000000..5b6f516
--- /dev/null
+++ b/pinot-controller/src/main/resources/app/pages/SchemaPageDetails.tsx
@@ -0,0 +1,308 @@
+/**
+ * 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 React, { useState, useEffect } from 'react';
+import { makeStyles } from '@material-ui/core/styles';
+import { Grid } from '@material-ui/core';
+import { RouteComponentProps, useHistory } from 'react-router-dom';
+import { UnControlled as CodeMirror } from 'react-codemirror2';
+import { TableData } from 'Models';
+import _ from 'lodash';
+import AppLoader from '../components/AppLoader';
+import CustomizedTables from '../components/Table';
+import 'codemirror/lib/codemirror.css';
+import 'codemirror/theme/material.css';
+import 'codemirror/mode/javascript/javascript';
+import 'codemirror/mode/sql/sql';
+import SimpleAccordion from '../components/SimpleAccordion';
+import PinotMethodUtils from '../utils/PinotMethodUtils';
+import EditConfigOp from '../components/Homepage/Operations/EditConfigOp';
+import { NotificationContext } from '../components/Notification/NotificationContext';
+import Utils from '../utils/Utils';
+import CustomButton from '../components/CustomButton';
+import Confirm from '../components/Confirm';
+
+const useStyles = makeStyles(() => ({
+ root: {
+ border: '1px #BDCCD9 solid',
+ borderRadius: 4,
+ marginBottom: '20px',
+ },
+ highlightBackground: {
+ border: '1px #4285f4 solid',
+ backgroundColor: 'rgba(66, 133, 244, 0.05)',
+ borderRadius: 4,
+ marginBottom: '20px',
+ },
+ body: {
+ borderTop: '1px solid #BDCCD9',
+ fontSize: '16px',
+ lineHeight: '3rem',
+ paddingLeft: '15px',
+ },
+ queryOutput: {
+ border: '1px solid #BDCCD9',
+ '& .CodeMirror': { height: 532 },
+ },
+ sqlDiv: {
+ border: '1px #BDCCD9 solid',
+ borderRadius: 4,
+ marginBottom: '20px',
+ },
+ operationDiv: {
+ border: '1px #BDCCD9 solid',
+ borderRadius: 4,
+ marginBottom: 20
+ }
+}));
+
+const jsonoptions = {
+ lineNumbers: true,
+ mode: 'application/json',
+ styleActiveLine: true,
+ gutters: ['CodeMirror-lint-markers'],
+ theme: 'default',
+ readOnly: true
+};
+
+type Props = {
+ tenantName: string;
+ schemaName: string;
+ instanceName: string;
+};
+
+type Summary = {
+ schemaName: string;
+ reportedSize: string | number;
+ estimatedSize: string | number;
+};
+
+const SchemaPageDetails = ({ match }: RouteComponentProps<Props>) => {
+ const { schemaName } = match.params;
+ const classes = useStyles();
+ const history = useHistory();
+ const [fetching, setFetching] = useState(true);
+ const [] = useState<Summary>({
+ schemaName: match.params.schemaName,
+ reportedSize: '',
+ estimatedSize: '',
+ });
+
+ const [] = React.useState({
+ enabled: true,
+ });
+
+ const [confirmDialog, setConfirmDialog] = React.useState(false);
+ const [dialogDetails, setDialogDetails] = React.useState(null);
+ const {dispatch} = React.useContext(NotificationContext);
+
+ const [showEditConfig, setShowEditConfig] = useState(false);
+ const [config, setConfig] = useState('{}');
+
+ const [tableSchema, setTableSchema] = useState<TableData>({
+ columns: [],
+ records: [],
+ });
+ const [tableConfig, setTableConfig] = useState('');
+ const [schemaJSON, setSchemaJSON] = useState(null);
+ const [actionType,setActionType] = useState(null);
+ const [schemaJSONFormat, setSchemaJSONFormat] = useState(false);
+
+ const fetchTableSchema = async () => {
+ const result = await PinotMethodUtils.getTableSchemaData(schemaName);
+ if(result.error){
+ setSchemaJSON(null);
+ setTableSchema({
+ columns: ['Column', 'Type', 'Field Type'],
+ records: []
+ });
+ } else {
+ setSchemaJSON(JSON.parse(JSON.stringify(result)));
+ const tableSchema = Utils.syncTableSchemaData(result, true);
+ setTableSchema(tableSchema);
+ }
+ fetchTableJSON();
+ };
+
+ const fetchTableJSON = async () => {
+ const result = await PinotMethodUtils.getSchemaData(schemaName);
+ setTableConfig(JSON.stringify(result, null, 2));
+ setFetching(false);
+ };
+
+ useEffect(()=>{
+ fetchTableSchema();
+ },[])
+
+ const handleConfigChange = (value: string) => {
+ setConfig(value);
+ };
+
+ const saveConfigAction = async () => {
+ let configObj = JSON.parse(config);
+ if(actionType === 'editTable'){
+ if(configObj.OFFLINE || configObj.REALTIME){
+ configObj = configObj.OFFLINE || configObj.REALTIME;
+ }
+ const result = await PinotMethodUtils.updateTable(schemaName, configObj);
+ syncResponse(result);
+ } else if(actionType === 'editSchema'){
+ const result = await PinotMethodUtils.updateSchema(schemaJSON.schemaName, configObj);
+ syncResponse(result);
+ }
+ };
+
+ const syncResponse = (result) => {
+ if(result.status){
+ dispatch({type: 'success', message: result.status, show: true});
+ setShowEditConfig(false);
+ fetchTableJSON();
+ } else {
+ dispatch({type: 'error', message: result.error, show: true});
+ }
+ };
+
+ const handleDeleteSchemaAction = () => {
+ setDialogDetails({
+ title: 'Delete Schema',
+ content: 'Are you sure want to delete this schema? Any tables using this schema might not function correctly.',
+ successCb: () => deleteSchema()
+ });
+ setConfirmDialog(true);
+ };
+
+ const deleteSchema = async () => {
+ const result = await PinotMethodUtils.deleteSchemaOp(schemaJSON.schemaName);
+ syncResponse(result);
+ history.push('/tables');
+ };
+
+ const closeDialog = () => {
+ setConfirmDialog(false);
+ setDialogDetails(null);
+ };
+
+ return fetching ? (
+ <AppLoader />
+ ) : (
+ <Grid
+ item
+ xs
+ style={{
+ padding: 20,
+ backgroundColor: 'white',
+ maxHeight: 'calc(100vh - 70px)',
+ overflowY: 'auto',
+ }}
+ >
+ <div className={classes.operationDiv}>
+ <SimpleAccordion
+ headerTitle="Operations"
+ showSearchBox={false}
+ >
+ <div>
+ <CustomButton
+ onClick={()=>{
+ setActionType('editSchema');
+ setConfig(JSON.stringify(schemaJSON, null, 2));
+ setShowEditConfig(true);
+ }}
+ tooltipTitle="Edit Schema"
+ enableTooltip={true}
+ >
+ Edit Schema
+ </CustomButton>
+ <CustomButton
+ isDisabled={!schemaJSON} onClick={handleDeleteSchemaAction}
+ tooltipTitle="Delete Schema"
+ enableTooltip={true}
+ >
+ Delete Schema
+ </CustomButton>
+ </div>
+ </SimpleAccordion>
+ </div>
+ <Grid container spacing={2}>
+ <Grid item xs={6}>
+ <div className={classes.sqlDiv}>
+ <SimpleAccordion
+ headerTitle="Schema Json"
+ showSearchBox={false}
+ >
+ <CodeMirror
+ options={jsonoptions}
+ value={tableConfig}
+ className={classes.queryOutput}
+ autoCursor={false}
+ />
+ </SimpleAccordion>
+ </div>
+ </Grid>
+ <Grid item xs={6}>
+ {!schemaJSONFormat ?
+ <CustomizedTables
+ title="Table Schema"
+ data={tableSchema}
+ isPagination={false}
+ noOfRows={tableSchema.records.length}
+ showSearchBox={true}
+ />
+ :
+ <div className={classes.sqlDiv}>
+ <SimpleAccordion
+ headerTitle="Table Schema"
+ showSearchBox={false}
+ accordionToggleObject={{
+ toggleName: "JSON Format",
+ toggleValue: schemaJSONFormat,
+ toggleChangeHandler: ()=>{setSchemaJSONFormat(!schemaJSONFormat);}
+ }}
+ >
+ <CodeMirror
+ options={jsonoptions}
+ value={JSON.stringify(schemaJSON, null, 2)}
+ className={classes.queryOutput}
+ autoCursor={false}
+ />
+ </SimpleAccordion>
+ </div>
+ }
+ </Grid>
+ </Grid>
+ <EditConfigOp
+ showModal={showEditConfig}
+ hideModal={()=>{setShowEditConfig(false);}}
+ saveConfig={saveConfigAction}
+ config={config}
+ handleConfigChange={handleConfigChange}
+ />
+ {confirmDialog && dialogDetails && <Confirm
+ openDialog={confirmDialog}
+ dialogTitle={dialogDetails.title}
+ dialogContent={dialogDetails.content}
+ successCallback={dialogDetails.successCb}
+ closeDialog={closeDialog}
+ dialogYesLabel='Yes'
+ dialogNoLabel='No'
+ />}
+ </Grid>
+ );
+};
+
+export default SchemaPageDetails;
diff --git a/pinot-controller/src/main/resources/app/pages/TablesListingPage.tsx b/pinot-controller/src/main/resources/app/pages/TablesListingPage.tsx
index fb41766..b233057 100644
--- a/pinot-controller/src/main/resources/app/pages/TablesListingPage.tsx
+++ b/pinot-controller/src/main/resources/app/pages/TablesListingPage.tsx
@@ -25,8 +25,9 @@ import PinotMethodUtils from '../utils/PinotMethodUtils';
import CustomizedTables from '../components/Table';
import CustomButton from '../components/CustomButton';
import SimpleAccordion from '../components/SimpleAccordion';
-import AddTableSchemaOp from '../components/Homepage/Operations/AddTableSchemaOp';
-import AddTableOp from '../components/Homepage/Operations/AddTableOp';
+import AddSchemaOp from '../components/Homepage/Operations/AddSchemaOp';
+import AddOfflineTableOp from '../components/Homepage/Operations/AddOfflineTableOp';
+import AddRealtimeTableOp from '../components/Homepage/Operations/AddRealtimeTableOp';
const useStyles = makeStyles(() => ({
gridContainer: {
@@ -46,12 +47,17 @@ const TablesListingPage = () => {
const classes = useStyles();
const [fetching, setFetching] = useState(true);
+ const [schemaDetails,setSchemaDetails] = useState<TableData>({
+ columns: [],
+ records: []
+ });
const [tableData, setTableData] = useState<TableData>({
columns: [],
records: []
});
- const [showAddTableSchemaModal, setShowAddTableSchemaModal] = useState(false);
- const [showAddTableModal, setShowAddTableModal] = useState(false);
+ const [showSchemaModal, setShowSchemaModal] = useState(false);
+ const [showAddOfflineTableModal, setShowAddOfflineTableModal] = useState(false);
+ const [showAddRealtimeTableModal, setShowAddRealtimeTableModal] = useState(false);
const fetchData = async () => {
!fetching && setFetching(true);
@@ -61,7 +67,9 @@ const TablesListingPage = () => {
tablesList.push(...record);
});
const tableDetails = await PinotMethodUtils.getAllTableDetails(tablesList);
+ const schemaDetailsData = await PinotMethodUtils.getAllSchemaDetails();
setTableData(tableDetails);
+ setSchemaDetails(schemaDetailsData)
setFetching(false);
};
@@ -80,18 +88,25 @@ const TablesListingPage = () => {
>
<div>
<CustomButton
- onClick={()=>{setShowAddTableSchemaModal(true)}}
- tooltipTitle="Add Schema & Table"
+ onClick={()=>{setShowSchemaModal(true)}}
+ tooltipTitle="Add Schema"
+ enableTooltip={true}
+ >
+ Add Schema
+ </CustomButton>
+ <CustomButton
+ onClick={()=>{setShowAddOfflineTableModal(true)}}
+ tooltipTitle="Add Offline Table"
enableTooltip={true}
>
- Add Schema & Table
+ Add Offline Table
</CustomButton>
<CustomButton
- onClick={()=>{setShowAddTableModal(true)}}
- tooltipTitle="Add Table"
+ onClick={()=>{setShowAddRealtimeTableModal(true)}}
+ tooltipTitle="Add Realtime Table"
enableTooltip={true}
>
- Add Table
+ Add Realtime Table
</CustomButton>
</div>
</SimpleAccordion>
@@ -105,18 +120,36 @@ const TablesListingPage = () => {
showSearchBox={true}
inAccordionFormat={true}
/>
+ <CustomizedTables
+ title="Schemas"
+ data={schemaDetails}
+ isPagination
+ showSearchBox={true}
+ inAccordionFormat={true}
+ addLinks
+ baseURL="/tenants/schema/"
+ />
+ {
+ showSchemaModal &&
+ <AddSchemaOp
+ hideModal={()=>{setShowSchemaModal(false);}}
+ fetchData={fetchData}
+ />
+ }
{
- showAddTableSchemaModal &&
- <AddTableSchemaOp
- hideModal={()=>{setShowAddTableSchemaModal(false);}}
+ showAddOfflineTableModal &&
+ <AddOfflineTableOp
+ hideModal={()=>{setShowAddOfflineTableModal(false);}}
fetchData={fetchData}
+ tableType={"OFFLINE"}
/>
}
{
- showAddTableModal &&
- <AddTableOp
- hideModal={()=>{setShowAddTableModal(false);}}
+ showAddRealtimeTableModal &&
+ <AddRealtimeTableOp
+ hideModal={()=>{setShowAddRealtimeTableModal(false);}}
fetchData={fetchData}
+ tableType={"REALTIME"}
/>
}
</Grid>
diff --git a/pinot-controller/src/main/resources/app/pages/TenantDetails.tsx b/pinot-controller/src/main/resources/app/pages/TenantDetails.tsx
index ccbeb18..9a44eee 100644
--- a/pinot-controller/src/main/resources/app/pages/TenantDetails.tsx
+++ b/pinot-controller/src/main/resources/app/pages/TenantDetails.tsx
@@ -152,7 +152,7 @@ const TenantPageDetails = ({ match }: RouteComponentProps<Props>) => {
const result = await PinotMethodUtils.getSegmentList(tableName);
const {columns, records, externalViewObj} = result;
const instanceObj = {};
- Object.keys(externalViewObj).map((segmentName)=>{
+ externalViewObj && Object.keys(externalViewObj).map((segmentName)=>{
const instanceKeys = Object.keys(externalViewObj[segmentName]);
instanceKeys.map((instanceName)=>{
if(!instanceObj[instanceName]){
diff --git a/pinot-controller/src/main/resources/app/requests/index.ts b/pinot-controller/src/main/resources/app/requests/index.ts
index c0fe697..26249f5 100644
--- a/pinot-controller/src/main/resources/app/requests/index.ts
+++ b/pinot-controller/src/main/resources/app/requests/index.ts
@@ -45,6 +45,9 @@ export const getTenantTableDetails = (tableName: string): Promise<AxiosResponse<
export const putTable = (name: string, params: string): Promise<AxiosResponse<OperationResponse>> =>
baseApi.put(`/tables/${name}`, params, { headers });
+export const getSchemaList = (): Promise<AxiosResponse<OperationResponse>> =>
+ baseApi.get(`/schemas`);
+
export const getSchema = (name: string): Promise<AxiosResponse<OperationResponse>> =>
baseApi.get(`/schemas/${name}`);
diff --git a/pinot-controller/src/main/resources/app/router.tsx b/pinot-controller/src/main/resources/app/router.tsx
index de0bd4f..ebd3c77 100644
--- a/pinot-controller/src/main/resources/app/router.tsx
+++ b/pinot-controller/src/main/resources/app/router.tsx
@@ -27,6 +27,7 @@ import QueryPage from './pages/Query';
import SegmentDetails from './pages/SegmentDetails';
import InstanceDetails from './pages/InstanceDetails';
import ZookeeperPage from './pages/ZookeeperPage';
+import SchemaPageDetails from './pages/SchemaPageDetails';
export default [
{ path: '/', Component: HomePage },
@@ -38,6 +39,7 @@ export default [
{ path: '/tables', Component: TablesListingPage },
{ path: '/tenants/:tenantName', Component: TenantsPage },
{ path: '/tenants/:tenantName/table/:tableName', Component: TenantPageDetails },
+ { path: '/tenants/schema/:schemaName', Component: SchemaPageDetails },
{ path: '/tenants/table/:tableName', Component: TenantPageDetails },
{ path: '/tenants/:tenantName/table/:tableName/:segmentName', Component: SegmentDetails },
{ path: '/instance/:instanceName', Component: InstanceDetails },
diff --git a/pinot-controller/src/main/resources/app/styles/styles.css b/pinot-controller/src/main/resources/app/styles/styles.css
index a2a7a6d..7f3bb68 100644
--- a/pinot-controller/src/main/resources/app/styles/styles.css
+++ b/pinot-controller/src/main/resources/app/styles/styles.css
@@ -89,4 +89,37 @@ li.CodeMirror-hint-active {
.CodeMirror-lint-tooltip{
z-index: 9999 !important;
+}
+
+h3.accordion-subtitle {
+ display: flex;
+ width: 100%;
+ justify-content: center;
+ align-items: center;
+ text-align: center;
+ color: #4285f3;
+ margin: 20px 0 0;
+}
+h3.accordion-subtitle:before, h3.accordion-subtitle:after {
+ content: "";
+ border-top: 2px #bdccd9 solid;
+ margin: 0 20px;
+ flex: 1 0 20px;
+}
+.chip {
+ margin: 2;
+ background-color: rgb(66 133 244 / 0.3) !important;
+}
+.chips {
+ display: flex;
+ flex-wrap: wrap;
+}
+
+.MuiListItem-root.Mui-selected, .MuiListItem-root.Mui-selected:hover {
+ background-color: rgb(66 133 244 / 0.3) !important;
+}
+.box-border{
+ border: 1px #ccc solid;
+ border-radius: 5px;
+ margin: 8px;
}
\ No newline at end of file
diff --git a/pinot-controller/src/main/resources/app/utils/PinotMethodUtils.ts b/pinot-controller/src/main/resources/app/utils/PinotMethodUtils.ts
index 011e8d7..592e562 100644
--- a/pinot-controller/src/main/resources/app/utils/PinotMethodUtils.ts
+++ b/pinot-controller/src/main/resources/app/utils/PinotMethodUtils.ts
@@ -62,7 +62,8 @@ import {
validateTable,
saveSchema,
saveTable,
- getSchema
+ getSchema,
+ getSchemaList
} from '../requests';
import Utils from './Utils';
@@ -307,6 +308,42 @@ const getTenantTableData = (tenantName) => {
});
};
+const getSchemaObject = async (schemaName) =>{
+ let schemaObj:Array<any> = [];
+ let {data} = await getSchema(schemaName);
+ console.log(data);
+ schemaObj.push(data.schemaName);
+ schemaObj.push(data.dimensionFieldSpecs ? data.dimensionFieldSpecs.length : 0);
+ schemaObj.push(data.dateTimeFieldSpecs ? data.dateTimeFieldSpecs.length : 0);
+ schemaObj.push(data.metricFieldSpecs ? data.metricFieldSpecs.length : 0);
+ schemaObj.push(schemaObj[1] + schemaObj[2] + schemaObj[3]);
+ return schemaObj;
+ }
+
+const getAllSchemaDetails = async () => {
+ const columnHeaders = ["Schema Name", "Dimension Columns", "Date-Time Columns", "Metrics Columns", "Total Columns"]
+ let schemaDetails:Array<any> = [];
+ let promiseArr = [];
+ const {data} = await getSchemaList()
+ promiseArr = data.map(async (o)=>{
+ return await getSchema(o);
+ });
+ const results = await Promise.all(promiseArr);
+ schemaDetails = results.map((obj)=>{
+ let schemaObj = [];
+ schemaObj.push(obj.data.schemaName);
+ schemaObj.push(obj.data.dimensionFieldSpecs ? obj.data.dimensionFieldSpecs.length : 0);
+ schemaObj.push(obj.data.dateTimeFieldSpecs ? obj.data.dateTimeFieldSpecs.length : 0);
+ schemaObj.push(obj.data.metricFieldSpecs ? obj.data.metricFieldSpecs.length : 0);
+ schemaObj.push(schemaObj[1] + schemaObj[2] + schemaObj[3]);
+ return schemaObj;
+ })
+ return {
+ columns: columnHeaders,
+ records: schemaDetails
+ };
+}
+
const getAllTableDetails = (tablesList) => {
const columnHeaders = [
'Table Name',
@@ -736,5 +773,6 @@ export default {
validateTableAction,
saveSchemaAction,
saveTableAction,
- getSchemaData
+ getSchemaData,
+ getAllSchemaDetails
};
---------------------------------------------------------------------
To unsubscribe, e-mail: commits-unsubscribe@pinot.apache.org
For additional commands, e-mail: commits-help@pinot.apache.org