KYLIN-3982 Add measures without purging segments
diff --git a/core-cube/src/main/java/org/apache/kylin/cube/CubeInstance.java b/core-cube/src/main/java/org/apache/kylin/cube/CubeInstance.java
index ad99377..e5fe614 100644
--- a/core-cube/src/main/java/org/apache/kylin/cube/CubeInstance.java
+++ b/core-cube/src/main/java/org/apache/kylin/cube/CubeInstance.java
@@ -40,9 +40,12 @@
import org.apache.kylin.cube.cuboid.CuboidScheduler;
import org.apache.kylin.cube.cuboid.TreeCuboidScheduler;
import org.apache.kylin.cube.model.CubeDesc;
+import org.apache.kylin.measure.MeasureInstance;
+import org.apache.kylin.measure.MeasureManager;
import org.apache.kylin.metadata.model.ColumnDesc;
import org.apache.kylin.metadata.model.DataModelDesc;
import org.apache.kylin.metadata.model.IBuildable;
+import org.apache.kylin.metadata.model.ISegment;
import org.apache.kylin.metadata.model.JoinTableDesc;
import org.apache.kylin.metadata.model.MeasureDesc;
import org.apache.kylin.metadata.model.SegmentRange;
@@ -182,7 +185,29 @@
}
public Segments<CubeSegment> getMergingSegments(CubeSegment mergedSegment) {
- return segments.getMergingSegments(mergedSegment);
+ Segments<CubeSegment> mergingSegments = segments.getMergingSegments(mergedSegment);
+ checkAlignedMeasure(mergingSegments);
+ return mergingSegments;
+ }
+
+ private boolean checkAlignedMeasure(Segments<CubeSegment> mergingSegments) {
+ int maxMeasureNumber = 0;
+ boolean isUnaligned = false;
+ MeasureManager measureManager = MeasureManager.getInstance(getConfig());
+ for (ISegment seg : mergingSegments) {
+ List<MeasureInstance> measuresOnSeg = measureManager.getMeasuresOnSegment(getName(), seg.getName());
+ if (measuresOnSeg.size() > maxMeasureNumber) {
+ if (maxMeasureNumber > 0) {
+ isUnaligned = true;
+ }
+ maxMeasureNumber = measuresOnSeg.size();
+ }
+ }
+ if (isUnaligned) {
+ throw new UnsupportedOperationException("Can't merge segment, because they have different measure number. Max measure number is "
+ + maxMeasureNumber + ", you can refresh other segments to make them aligned.");
+ }
+ return true;
}
public CubeSegment getOriginalSegmentToRefresh(CubeSegment refreshedSegment) {
diff --git a/core-cube/src/main/java/org/apache/kylin/cube/gridtable/CuboidToGridTableMappingFilterNullCol.java b/core-cube/src/main/java/org/apache/kylin/cube/gridtable/CuboidToGridTableMappingFilterNullCol.java
index 6d06ed4..c701fdb 100644
--- a/core-cube/src/main/java/org/apache/kylin/cube/gridtable/CuboidToGridTableMappingFilterNullCol.java
+++ b/core-cube/src/main/java/org/apache/kylin/cube/gridtable/CuboidToGridTableMappingFilterNullCol.java
@@ -28,6 +28,7 @@
import org.apache.kylin.metadata.model.FunctionDesc;
import org.apache.kylin.metadata.model.TblColRef;
+import java.util.BitSet;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
@@ -131,6 +132,19 @@
}
@Override
+ public ImmutableBitSet makeGridTableColumns(Collection<? extends FunctionDesc> metrics) {
+ BitSet result = new BitSet();
+ for (FunctionDesc metric : metrics) {
+ int idx = getIndexOf(metric);
+ if (idx < 0) {
+ continue;
+ }
+ result.set(idx);
+ }
+ return new ImmutableBitSet(result);
+ }
+
+ @Override
public String[] makeAggrFuncs(Collection<FunctionDesc> metrics) {
List<FunctionDesc> metricList = Lists.newArrayListWithCapacity(metrics.size());
metrics.stream().forEach(m -> {
diff --git a/core-cube/src/main/java/org/apache/kylin/cube/model/CubeDesc.java b/core-cube/src/main/java/org/apache/kylin/cube/model/CubeDesc.java
index 8b45837..3d0c7e5 100644
--- a/core-cube/src/main/java/org/apache/kylin/cube/model/CubeDesc.java
+++ b/core-cube/src/main/java/org/apache/kylin/cube/model/CubeDesc.java
@@ -568,10 +568,10 @@
.append(JsonUtil.writeValueAsString(this.modelName)).append("|")//
.append(JsonUtil.writeValueAsString(this.nullStrings)).append("|")//
.append(JsonUtil.writeValueAsString(this.dimensions)).append("|")//
- .append(JsonUtil.writeValueAsString(this.measures)).append("|")//
+ //.append(JsonUtil.writeValueAsString(this.measures)).append("|")//
.append(JsonUtil.writeValueAsString(this.rowkey)).append("|")//
.append(JsonUtil.writeValueAsString(this.aggregationGroups)).append("|")//
- .append(JsonUtil.writeValueAsString(this.hbaseMapping)).append("|")//
+ //.append(JsonUtil.writeValueAsString(this.hbaseMapping)).append("|")//
.append(JsonUtil.writeValueAsString(this.storageType)).append("|");
if (mandatoryDimensionSetList != null && !mandatoryDimensionSetList.isEmpty()) {
diff --git a/core-cube/src/main/java/org/apache/kylin/gridtable/GTInfo.java b/core-cube/src/main/java/org/apache/kylin/gridtable/GTInfo.java
index 739adf8..e85724f 100644
--- a/core-cube/src/main/java/org/apache/kylin/gridtable/GTInfo.java
+++ b/core-cube/src/main/java/org/apache/kylin/gridtable/GTInfo.java
@@ -205,8 +205,8 @@
for (int i = 0; i < colBlocks.length; i++) {
merge = merge.or(colBlocks[i]);
}
- if (!merge.equals(colAll))
- throw new IllegalStateException();
+// if (!merge.equals(colAll))
+// throw new IllegalStateException();
// primary key must be the first column block
if (!primaryKey.equals(colBlocks[0]))
diff --git a/server-base/src/main/java/org/apache/kylin/rest/controller/CubeController.java b/server-base/src/main/java/org/apache/kylin/rest/controller/CubeController.java
index c3f45a6..8b92c00 100644
--- a/server-base/src/main/java/org/apache/kylin/rest/controller/CubeController.java
+++ b/server-base/src/main/java/org/apache/kylin/rest/controller/CubeController.java
@@ -44,6 +44,7 @@
import org.apache.kylin.cube.model.CubeJoinedFlatTableDesc;
import org.apache.kylin.cube.model.HBaseColumnDesc;
import org.apache.kylin.cube.model.HBaseColumnFamilyDesc;
+import org.apache.kylin.cube.model.HBaseMappingDesc;
import org.apache.kylin.cube.model.RowKeyColDesc;
import org.apache.kylin.dimension.DimensionEncodingFactory;
import org.apache.kylin.engine.mr.common.CuboidStatsReaderUtil;
@@ -55,6 +56,7 @@
import org.apache.kylin.metadata.model.MeasureDesc;
import org.apache.kylin.metadata.model.SegmentRange;
import org.apache.kylin.metadata.model.SegmentRange.TSRange;
+import org.apache.kylin.metadata.model.SegmentStatusEnum;
import org.apache.kylin.metadata.project.ProjectInstance;
import org.apache.kylin.metadata.realization.RealizationStatusEnum;
import org.apache.kylin.rest.exception.BadRequestException;
@@ -77,6 +79,7 @@
import org.apache.kylin.rest.response.ResponseCode;
import org.apache.kylin.rest.service.CubeService;
import org.apache.kylin.rest.service.JobService;
+import org.apache.kylin.rest.service.MeasureService;
import org.apache.kylin.rest.service.ProjectService;
import org.apache.kylin.rest.util.ValidateUtil;
import org.apache.kylin.source.kafka.util.KafkaClient;
@@ -121,6 +124,10 @@
@Qualifier("projectService")
private ProjectService projectService;
+ @Autowired
+ @Qualifier("measureMgmtService")
+ public MeasureService measureService;
+
@RequestMapping(value = "/validate/{cubeName}", method = RequestMethod.GET, produces = { "application/json" })
@ResponseBody
public EnvelopeResponse<Boolean> validateModelName(@PathVariable String cubeName) {
@@ -653,6 +660,12 @@
updateRequest(cubeRequest, false, error);
return cubeRequest;
}
+ Pair<List<String>, List<String>> updateMeasures = cubeService.getUpdateMeasures(desc, cubeService.getCubeDescManager().getCubeDesc(desc.getName()));
+ if (cube.getSegments().size() > 0 && updateMeasures.getFirst().size() > 0) {
+ // add dynamic measure to HBase column family mapping
+ HBaseMappingDesc newHBaseMapping = cubeService.getAutoHBaseMapping(desc, updateMeasures.getFirst());
+ desc.setHbaseMapping(newHBaseMapping);
+ }
validateColumnFamily(desc);
@@ -735,6 +748,13 @@
hr.setSourceOffsetStart((Long) segment.getSegRange().start.v);
hr.setSourceOffsetEnd((Long) segment.getSegRange().end.v);
}
+ // add info about Measure on this segment
+ if (hr.getSegmentStatus().equals(SegmentStatusEnum.READY.toString())) {
+ List<String> measuresOnSegment = measureService.getMeasuresOnSegment(cubeName, hr.getSegmentName());
+ hr.setMeasuresOnSegment(measuresOnSegment);
+ } else {
+ hr.setMeasuresOnSegment(Collections.EMPTY_LIST);
+ }
hbase.add(hr);
}
diff --git a/server-base/src/main/java/org/apache/kylin/rest/controller/MeasureController.java b/server-base/src/main/java/org/apache/kylin/rest/controller/MeasureController.java
new file mode 100644
index 0000000..a50f165
--- /dev/null
+++ b/server-base/src/main/java/org/apache/kylin/rest/controller/MeasureController.java
@@ -0,0 +1,113 @@
+/*
+ * 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.
+ */
+
+package org.apache.kylin.rest.controller;
+
+import com.google.common.collect.Maps;
+import org.apache.kylin.common.KylinConfig;
+import org.apache.kylin.metadata.model.DateTimeRange;
+import org.apache.kylin.rest.response.SegmentResponse;
+import org.apache.kylin.rest.service.MeasureService;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.stereotype.Controller;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.ResponseBody;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+@Controller
+@RequestMapping(value = "/measure")
+public class MeasureController extends BasicController{
+ private static final Logger LOG = LoggerFactory.getLogger(MeasureController.class);
+
+ @Autowired
+ @Qualifier("measureMgmtService")
+ public MeasureService measureService;
+
+
+ @GetMapping(value = "/segment/{cubeName}/{measureName}", produces = { "application/json" })
+ @ResponseBody
+ public SegmentResponse getSegmentRanges(@PathVariable String cubeName,
+ @PathVariable String measureName) {
+ if (!getConfig().isEditableMetricCube()) {
+ return new SegmentResponse();
+ }
+ List<DateTimeRange> dtrs = measureService.getSegmentRanges(cubeName, measureName);
+ int segCnt = measureService.getSegmentCountOf(cubeName, measureName);
+ SegmentResponse ret = new SegmentResponse();
+ ret.setSegmentCount(segCnt);
+ ret.setTimeRanges(dtrs.stream().map(dr -> dr.toString()).collect(Collectors.toList()));
+ return ret;
+ }
+
+ @GetMapping(value = "/segment/{cubeName}", produces = { "application/json" })
+ @ResponseBody
+ public Map<String, SegmentResponse> getSegmentRanges(@PathVariable String cubeName) {
+ if (!getConfig().isEditableMetricCube()) {
+ return Collections.EMPTY_MAP;
+ }
+ Map<String, List<DateTimeRange>> measureVsegment = measureService.getMeasureSegmentsPair(cubeName);
+ Map<String, SegmentResponse> ret = Maps.newHashMapWithExpectedSize(measureVsegment.size());
+ for (Map.Entry<String, List<DateTimeRange>> entry : measureVsegment.entrySet()) {
+ SegmentResponse sr = new SegmentResponse();
+ sr.setTimeRanges(entry.getValue().stream().map(dr -> dr.toString()).collect(Collectors.toList()));
+ sr.setSegmentCount(measureService.getSegmentCountOf(cubeName, entry.getKey()));
+ ret.put(entry.getKey(), sr);
+ }
+ return ret;
+ }
+
+
+ @GetMapping(value = "/{cubeName}", produces = { "application/json" })
+ @ResponseBody
+ public Map<String, List<String>> getMeasuresOnSegment(@PathVariable String cubeName) {
+ if (!getConfig().isEditableMetricCube()) {
+ return Collections.EMPTY_MAP;
+ }
+ Map<String, List<String>> segmentMeasurePair = measureService.getMeasuresOnSegment(cubeName);
+ return segmentMeasurePair;
+ }
+
+ @GetMapping(value = "/{cubeName}/{segmentName}", produces = { "application/json" })
+ @ResponseBody
+ public List<String> getMeasuresOnSegment(@PathVariable String cubeName,
+ @PathVariable String segmentName) {
+ if (!getConfig().isEditableMetricCube()) {
+ return Collections.EMPTY_LIST;
+ }
+ List<String> measureNames = measureService.getMeasuresOnSegment(cubeName, segmentName);
+ return measureNames;
+ }
+
+ public KylinConfig getConfig() {
+ KylinConfig kylinConfig = KylinConfig.getInstanceFromEnv();
+ if (kylinConfig == null) {
+ throw new IllegalArgumentException("Failed to load kylin config instance");
+ }
+ return kylinConfig;
+ }
+
+}
diff --git a/server-base/src/main/java/org/apache/kylin/rest/response/CubeInstanceResponse.java b/server-base/src/main/java/org/apache/kylin/rest/response/CubeInstanceResponse.java
index f6f88bd..22b8464 100644
--- a/server-base/src/main/java/org/apache/kylin/rest/response/CubeInstanceResponse.java
+++ b/server-base/src/main/java/org/apache/kylin/rest/response/CubeInstanceResponse.java
@@ -22,6 +22,7 @@
import org.apache.kylin.metadata.model.ISourceAware;
import com.fasterxml.jackson.annotation.JsonProperty;
+import org.apache.kylin.metadata.realization.RealizationStatusEnum;
/**
*/
@@ -46,6 +47,8 @@
private long inputRecordCnt;
@JsonProperty("input_records_size")
private long inputRecordSizeBytes;
+ @JsonProperty("can_add_measure")
+ private boolean canAddMeasure;
public CubeInstanceResponse(CubeInstance cube, String project) {
@@ -81,6 +84,7 @@
initSizeKB();
initInputRecordCount();
initInputRecordSizeBytes();
+ this.canAddMeasure = checkCanAddMeasure(cube);
}
protected void setModel(String model) {
@@ -99,4 +103,16 @@
this.inputRecordSizeBytes = super.getInputRecordSizeBytes();
}
+ private boolean checkCanAddMeasure(CubeInstance cube) {
+ if (!cube.getConfig().isEditableMetricCube()) {
+ return false;
+ }
+ if (!cube.getStatus().equals(RealizationStatusEnum.DISABLED)) {
+ return false;
+ }
+ if (cube.getSegments().size() == 0) {
+ return false;
+ }
+ return true;
+ }
}
diff --git a/server-base/src/main/java/org/apache/kylin/rest/response/HBaseResponse.java b/server-base/src/main/java/org/apache/kylin/rest/response/HBaseResponse.java
index 6d17a53..e365827 100644
--- a/server-base/src/main/java/org/apache/kylin/rest/response/HBaseResponse.java
+++ b/server-base/src/main/java/org/apache/kylin/rest/response/HBaseResponse.java
@@ -19,6 +19,7 @@
package org.apache.kylin.rest.response;
import java.io.Serializable;
+import java.util.List;
public class HBaseResponse implements Serializable {
private static final long serialVersionUID = 7263557115683263492L;
@@ -33,6 +34,7 @@
private long sourceOffsetStart;
private long sourceOffsetEnd;
private long sourceCount;
+ private List<String> measuresOnSegment;
public HBaseResponse() {
}
@@ -159,4 +161,12 @@
public void setSourceCount(long sourceCount) {
this.sourceCount = sourceCount;
}
+
+ public List<String> getMeasuresOnSegment() {
+ return measuresOnSegment;
+ }
+
+ public void setMeasuresOnSegment(List<String> measuresOnSegment) {
+ this.measuresOnSegment = measuresOnSegment;
+ }
}
diff --git a/server-base/src/main/java/org/apache/kylin/rest/response/SQLResponse.java b/server-base/src/main/java/org/apache/kylin/rest/response/SQLResponse.java
index 1721efe..40da475 100644
--- a/server-base/src/main/java/org/apache/kylin/rest/response/SQLResponse.java
+++ b/server-base/src/main/java/org/apache/kylin/rest/response/SQLResponse.java
@@ -83,6 +83,9 @@
// indicating the lazy query start time, -1 indicating not enabled
protected long lazyQueryStartTime = -1L;
+ // show the missing Measure within the query scope
+ protected List<String> missMeasureMessage;
+
public SQLResponse() {
}
@@ -257,4 +260,12 @@
this.queryStatistics = null;
}
}
+
+ public List<String> getMissMeasureMessage() {
+ return missMeasureMessage;
+ }
+
+ public void setMissMeasureMessage(List<String> missMeasureMessage) {
+ this.missMeasureMessage = missMeasureMessage;
+ }
}
diff --git a/server-base/src/main/java/org/apache/kylin/rest/response/SegmentResponse.java b/server-base/src/main/java/org/apache/kylin/rest/response/SegmentResponse.java
new file mode 100644
index 0000000..ba2d428
--- /dev/null
+++ b/server-base/src/main/java/org/apache/kylin/rest/response/SegmentResponse.java
@@ -0,0 +1,47 @@
+/*
+ * 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.
+ */
+
+package org.apache.kylin.rest.response;
+
+import com.fasterxml.jackson.annotation.JsonProperty;
+
+import java.util.List;
+
+public class SegmentResponse {
+
+ @JsonProperty("segment_count")
+ private int segmentCount;
+ @JsonProperty("time_ranges")
+ private List<String> timeRanges;
+
+ public int getSegmentCount() {
+ return segmentCount;
+ }
+
+ public void setSegmentCount(int segmentCount) {
+ this.segmentCount = segmentCount;
+ }
+
+ public List<String> getTimeRanges() {
+ return timeRanges;
+ }
+
+ public void setTimeRanges(List<String> timeRanges) {
+ this.timeRanges = timeRanges;
+ }
+}
diff --git a/server-base/src/main/java/org/apache/kylin/rest/service/BasicService.java b/server-base/src/main/java/org/apache/kylin/rest/service/BasicService.java
index 9ac2602..8dc3379 100644
--- a/server-base/src/main/java/org/apache/kylin/rest/service/BasicService.java
+++ b/server-base/src/main/java/org/apache/kylin/rest/service/BasicService.java
@@ -24,6 +24,7 @@
import org.apache.kylin.cube.CubeDescManager;
import org.apache.kylin.cube.CubeManager;
import org.apache.kylin.job.execution.ExecutableManager;
+import org.apache.kylin.measure.MeasureManager;
import org.apache.kylin.metadata.TableMetadataManager;
import org.apache.kylin.metadata.acl.TableACLManager;
import org.apache.kylin.metadata.badquery.BadQueryHistoryManager;
@@ -98,4 +99,8 @@
public MetricsManager getMetricsManager() {
return MetricsManager.getInstance();
}
+
+ public MeasureManager getMeasureManager() {
+ return MeasureManager.getInstance(getConfig());
+ }
}
diff --git a/server-base/src/main/java/org/apache/kylin/rest/service/CubeService.java b/server-base/src/main/java/org/apache/kylin/rest/service/CubeService.java
index 7378165..8cd3bee 100644
--- a/server-base/src/main/java/org/apache/kylin/rest/service/CubeService.java
+++ b/server-base/src/main/java/org/apache/kylin/rest/service/CubeService.java
@@ -20,6 +20,7 @@
import java.io.IOException;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.EnumSet;
@@ -27,6 +28,7 @@
import java.util.Locale;
import java.util.Map;
import java.util.Set;
+import java.util.stream.Collectors;
import org.apache.commons.lang.StringUtils;
import org.apache.hadoop.hbase.client.HBaseAdmin;
@@ -39,10 +41,15 @@
import org.apache.kylin.cube.CubeManager;
import org.apache.kylin.cube.CubeSegment;
import org.apache.kylin.cube.CubeUpdate;
+import org.apache.kylin.cube.adapter.AbstractHBaseMappingAdapter;
+import org.apache.kylin.cube.adapter.IWiseHBaseMapper;
import org.apache.kylin.cube.cuboid.Cuboid;
import org.apache.kylin.cube.cuboid.CuboidCLI;
import org.apache.kylin.cube.cuboid.CuboidScheduler;
import org.apache.kylin.cube.model.CubeDesc;
+import org.apache.kylin.cube.model.HBaseColumnDesc;
+import org.apache.kylin.cube.model.HBaseColumnFamilyDesc;
+import org.apache.kylin.cube.model.HBaseMappingDesc;
import org.apache.kylin.engine.EngineFactory;
import org.apache.kylin.engine.mr.CubingJob;
import org.apache.kylin.engine.mr.JobBuilderSupport;
@@ -393,6 +400,12 @@
}
this.releaseAllSegments(cube);
+ // All measures in a column family are stored in one column will reduce the
+ // complexity in Web UI
+ if (cube.getSegments().size() == 0) {
+ mergeMeasureIntoOneCF(cube.getDescriptor());
+ }
+
return cube;
}
@@ -606,7 +619,9 @@
CubeInstance cubeInstance = CubeManager.getInstance(getConfig()).updateCubeDropSegments(cube, toDelete);
cleanSegmentStorage(Collections.singletonList(toDelete));
-
+ if (cube.getSegments().size() == 0) {
+ mergeMeasureIntoOneCF(cube.getDescriptor());
+ }
return cubeInstance;
}
@@ -1079,4 +1094,39 @@
throw new InternalErrorException("Failed to perform one-click migrating", e);
}
}
+
+ public Pair<List<String>, List<String>> getUpdateMeasures(CubeDesc newDesc, CubeDesc oldDesc) {
+ List<String> oldMeasures = oldDesc.getMeasures().stream().map(MeasureDesc::getName).collect(Collectors.toList());
+ List<String> newMeasures = newDesc.getMeasures().stream().map(MeasureDesc::getName).collect(Collectors.toList());
+
+ List<String> needAdd = Lists.newArrayList(newMeasures);
+ needAdd.removeAll(oldMeasures);
+
+ List<String> needDrop = Lists.newArrayList(oldMeasures);
+ needDrop.removeAll(newMeasures);
+ Pair<List<String>, List<String>> ret = new Pair<>(needAdd, needDrop);
+ return ret;
+ }
+
+ public HBaseMappingDesc getAutoHBaseMapping(CubeDesc desc, List<String> needAddMeasure) {
+ IWiseHBaseMapper mapper = AbstractHBaseMappingAdapter.getHBaseAdapter(getConfig());
+ return mapper.addMeasure(desc, needAddMeasure);
+ }
+
+ private HBaseMappingDesc mergeMeasureIntoOneCF(CubeDesc desc) {
+ HBaseMappingDesc hBaseMappingDesc = desc.getHbaseMapping();
+ for (HBaseColumnFamilyDesc cfDesc : hBaseMappingDesc.getColumnFamily()) {
+ if (cfDesc.getColumns().length < 2) {
+ continue;
+ }
+ List<String> measuresInCF = Lists.newArrayList();
+ for (HBaseColumnDesc colDesc : cfDesc.getColumns()) {
+ measuresInCF.addAll(Arrays.asList(colDesc.getMeasureRefs()));
+ }
+ cfDesc.setColumns(new HBaseColumnDesc[]{cfDesc.getColumns()[0]});
+ cfDesc.getColumns()[0].setMeasureRefs(measuresInCF.toArray(new String[0]));
+ }
+
+ return hBaseMappingDesc;
+ }
}
diff --git a/server-base/src/main/java/org/apache/kylin/rest/service/MeasureService.java b/server-base/src/main/java/org/apache/kylin/rest/service/MeasureService.java
new file mode 100644
index 0000000..07b3dcf
--- /dev/null
+++ b/server-base/src/main/java/org/apache/kylin/rest/service/MeasureService.java
@@ -0,0 +1,125 @@
+/*
+ * 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.
+ */
+
+package org.apache.kylin.rest.service;
+
+import com.google.common.collect.Maps;
+import org.apache.kylin.cube.CubeInstance;
+import org.apache.kylin.cube.model.CubeDesc;
+import org.apache.kylin.measure.MeasureInstance;
+import org.apache.kylin.metadata.model.DateTimeRange;
+import org.apache.kylin.metadata.model.ISegment;
+import org.apache.kylin.metadata.model.MeasureDesc;
+import org.apache.kylin.metadata.model.SegmentStatusEnum;
+import org.apache.kylin.rest.exception.BadRequestException;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.stereotype.Service;
+
+import java.util.Collections;
+import java.util.List;
+import java.util.Locale;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+import static java.lang.String.format;
+
+@Service("measureMgmtService")
+public class MeasureService extends BasicService{
+ private final static Logger LOG = LoggerFactory.getLogger(MeasureService.class);
+
+ public List<DateTimeRange> getSegmentRanges(String cubeName, String measureName) {
+ CubeDesc cube = getCubeDesc(cubeName);
+ if (cube == null) {
+ return Collections.EMPTY_LIST;
+ }
+ return getSegmentRanges(cube, measureName);
+ }
+
+ public Map<String, List<DateTimeRange>> getMeasureSegmentsPair(String cubeName) {
+ Map<String, List<DateTimeRange>> ret = Maps.newHashMap();
+ CubeDesc cube = getCubeDesc(cubeName);
+ if (cube == null) {
+ return Collections.EMPTY_MAP;
+ }
+ List<MeasureInstance> measuresInCache = getMeasureManager().getMeasuresInCube(cubeName);
+ List<MeasureDesc> measuresInCube = cube.getMeasures();
+ if (measuresInCache.size() != measuresInCube.size()) {
+ throw new IllegalStateException(String.format(Locale.ROOT, "The size of measures not equal. in cache: [%s], in cube: [%s]",
+ measuresInCache.stream().map(m -> m.getName()).collect(Collectors.joining(", ")),
+ measuresInCube.stream().map(m -> m.getName()).collect(Collectors.joining(", "))));
+ }
+ for (MeasureDesc measureDesc : measuresInCube) {
+ ret.put(measureDesc.getName(), getSegmentRanges(cube, measureDesc.getName()));
+ }
+ return ret;
+ }
+
+ public int getSegmentCountOf(String cubeName, String measureName) {
+ MeasureInstance measure = getMeasureManager().getMeasure(cubeName, measureName);
+ return null == measure ? 0 : measure.getSegments().size();
+ }
+
+ public List<DateTimeRange> getSegmentRanges(CubeDesc cube, String measureName) {
+ MeasureDesc measure = findMeasureByName(cube, measureName);
+ MeasureInstance measureInstance = getMeasureManager().getMeasure(cube.getName(), measure.getName());
+ if (measureInstance == null) {
+ return Collections.EMPTY_LIST;
+ }
+ return measureInstance.getDateTimeRanges();
+ }
+
+ public List<String> getMeasuresOnSegment(String cubeName, String segmentName) {
+ return getMeasureManager()
+ .getMeasuresOnSegment(cubeName, segmentName)
+ .stream()
+ .map(MeasureInstance::getName)
+ .collect(Collectors.toList());
+ }
+
+ public Map<String, List<String>> getMeasuresOnSegment(String cubeName) {
+ CubeDesc cubeDesc = getCubeDesc(cubeName);
+ if (null == cubeDesc) {
+ return Collections.EMPTY_MAP;
+ }
+ CubeInstance cubeInstance = getCubeManager().getCube(cubeDesc.getName());
+ Map<String, List<String>> ret = Maps.newHashMap();
+ for (ISegment seg : cubeInstance.getSegments(SegmentStatusEnum.READY)) {
+ ret.put(seg.getName(), getMeasuresOnSegment(cubeName, seg.getName()));
+ }
+ return ret;
+ }
+
+ private CubeDesc getCubeDesc(String cubeName) {
+ CubeDesc cube = getCubeDescManager().getCubeDesc(cubeName);
+ if (null == cube) {
+ LOG.warn("Can't find cube {}, may be it's designning?", cubeName);
+ return null;
+ }
+ return cube;
+ }
+
+ private MeasureDesc findMeasureByName(CubeDesc cube, String measureName) {
+ for (MeasureDesc measureDesc : cube.getMeasures()) {
+ if (measureName.equals(measureDesc.getName())) {
+ return measureDesc;
+ }
+ }
+ throw new BadRequestException(format(Locale.ROOT, "Can't find measure[%s] in cube[%s]", measureName, cube.getName()));
+ }
+}
diff --git a/server-base/src/main/java/org/apache/kylin/rest/service/QueryService.java b/server-base/src/main/java/org/apache/kylin/rest/service/QueryService.java
index 59b2b61..b1ed290 100644
--- a/server-base/src/main/java/org/apache/kylin/rest/service/QueryService.java
+++ b/server-base/src/main/java/org/apache/kylin/rest/service/QueryService.java
@@ -45,6 +45,7 @@
import java.util.Map;
import java.util.NoSuchElementException;
import java.util.Set;
+import java.util.stream.Collectors;
import javax.annotation.PostConstruct;
@@ -64,6 +65,7 @@
import org.apache.commons.pool2.impl.GenericKeyedObjectPoolConfig;
import org.apache.kylin.cache.cachemanager.MemcachedCacheManager;
import org.apache.kylin.common.KylinConfig;
+import org.apache.kylin.common.MissingMeasureSegment;
import org.apache.kylin.common.QueryContext;
import org.apache.kylin.common.QueryContextFacade;
import org.apache.kylin.common.debug.BackdoorToggles;
@@ -440,7 +442,12 @@
}
if (sqlResponse.getIsException())
throw new InternalErrorException(sqlResponse.getExceptionMessage());
-
+ // add hint about the missing measure within the query
+ List<MissingMeasureSegment> missingMeasureSegmentList = queryContext.getMissingMeasureSegments();
+ List<String> missMeasureMsgs = missingMeasureSegmentList.stream()
+ .map(mms -> mms.getMissingMsg())
+ .collect(Collectors.toList());
+ sqlResponse.setMissMeasureMessage(missMeasureMsgs);
return sqlResponse;
} finally {
diff --git a/webapp/app/js/controllers/cubeAdvanceSetting.js b/webapp/app/js/controllers/cubeAdvanceSetting.js
index 08cd044..958c65a 100755
--- a/webapp/app/js/controllers/cubeAdvanceSetting.js
+++ b/webapp/app/js/controllers/cubeAdvanceSetting.js
@@ -330,9 +330,11 @@
$scope.getAssignedMeasureNames = function () {
var assignedMeasures = [];
angular.forEach($scope.cubeMetaFrame.hbase_mapping.column_family, function (colFamily, index) {
- angular.forEach(colFamily.columns[0].measure_refs, function (measure, index) {
- assignedMeasures.push(measure);
- });
+ angular.forEach(colFamily.columns, function (column, index) {
+ angular.forEach(column.measure_refs, function (measure, index) {
+ assignedMeasures.push(measure);
+ });
+ })
});
return assignedMeasures;
};
@@ -342,14 +344,20 @@
var tmpColumnFamily = $scope.cubeMetaFrame.hbase_mapping.column_family;
for(var j=0;j<$scope.cubeMetaFrame.hbase_mapping.column_family.length; j++) {
- for (var i=0;i<$scope.cubeMetaFrame.hbase_mapping.column_family[j].columns[0].measure_refs.length; i++){
- var allIndex = allMeasureNames.indexOf($scope.cubeMetaFrame.hbase_mapping.column_family[j].columns[0].measure_refs[i]);
- if (allIndex == -1) {
- tmpColumnFamily[j].columns[0].measure_refs.splice(i, 1);
- i--
+ for (var k=0; k < $scope.cubeMetaFrame.hbase_mapping.column_family[j].columns.length; k++) {
+ for (var i=0;i<$scope.cubeMetaFrame.hbase_mapping.column_family[j].columns[k].measure_refs.length; i++){
+ var allIndex = allMeasureNames.indexOf($scope.cubeMetaFrame.hbase_mapping.column_family[j].columns[0].measure_refs[i]);
+ if (allIndex == -1) {
+ tmpColumnFamily[j].columns[0].measure_refs.splice(i, 1);
+ i--
+ }
+ }
+ if (tmpColumnFamily[j].columns[k].measure_refs.length == 0) {
+ tmpColumnFamily[j].columns.splice(k, 1);
+ k--
}
}
- if (tmpColumnFamily[j].columns[0].measure_refs.length == 0) {
+ if (tmpColumnFamily[j].columns.length == 0) {
tmpColumnFamily.splice(j, 1);
j--
}
@@ -524,4 +532,12 @@
};
$scope.cubeLookups = $scope.getCubeLookups();
+
+ $scope.getCFDisplayString = function (columnFamily) {
+ var columnArr = new Array();
+ for (i = 0; i < columnFamily.columns.length; i++) {
+ columnArr[i] = columnFamily.columns[i].qualifier + ":[" + columnFamily.columns[i].measure_refs + "]"
+ }
+ return columnArr.join(", ");
+ };
});
diff --git a/webapp/app/js/controllers/cubeEdit.js b/webapp/app/js/controllers/cubeEdit.js
index eeed96a..6201eca 100755
--- a/webapp/app/js/controllers/cubeEdit.js
+++ b/webapp/app/js/controllers/cubeEdit.js
@@ -28,6 +28,14 @@
var absUrl = $location.absUrl();
$scope.cubeMode = absUrl.indexOf("/cubes/add") != -1 ? 'addNewCube' : absUrl.indexOf("/cubes/edit") != -1 ? 'editExistCube' : 'default';
+ $scope.cubeMode2 = 'default';
+ $scope.isMeasureEdit = false;
+ if ($scope.cubeMode === 'default' && absUrl.indexOf("/cubes/measure") != -1) {
+ $scope.cubeMode2 = 'measure';
+ $scope.isMeasureEdit = true;
+ $scope.cubeMode = 'editExistCube';
+ }
+
if ($scope.cubeMode == "addNewCube" &&ProjectModel.selectedProject==null) {
SweetAlert.swal('Oops...', 'Please select your project first.', 'warning');
$location.path("/models");
diff --git a/webapp/app/js/controllers/cubeMeasures.js b/webapp/app/js/controllers/cubeMeasures.js
index 79d21e5..5313ed0 100644
--- a/webapp/app/js/controllers/cubeMeasures.js
+++ b/webapp/app/js/controllers/cubeMeasures.js
@@ -18,10 +18,11 @@
'use strict';
-KylinApp.controller('CubeMeasuresCtrl', function ($scope, $modal,MetaModel,cubesManager,CubeDescModel,SweetAlert,VdmUtil,TableModel,cubeConfig,modelsManager,kylinConfig) {
+KylinApp.controller('CubeMeasuresCtrl', function ($scope, $modal,MetaModel,cubesManager,CubeDescModel,SweetAlert,VdmUtil,TableModel,cubeConfig,modelsManager,kylinConfig,MeasureService) {
$scope.num=0;
$scope.convertedColumns=[];
$scope.groupby=[];
+ $scope.newMeasures=[];
$scope.initUpdateMeasureStatus = function(){
$scope.updateMeasureStatus = {
isEdit:false,
@@ -250,9 +251,16 @@
if($scope.updateMeasureStatus.isEdit == true){
$scope.cubeMetaFrame.measures[$scope.updateMeasureStatus.editIndex] = $scope.newMeasure;
+ if ($scope.isMeasureEdit) {
+ $scope.removeElement($scope.newMeasures, $scope.cubeMetaFrame.measures[$scope.updateMeasureStatus.editIndex]);
+ $scope.newMeasures.push($scope.newMeasure);
+ }
}
else {
$scope.cubeMetaFrame.measures.push($scope.newMeasure);
+ if ($scope.isMeasureEdit) {
+ $scope.newMeasures.push($scope.newMeasure);
+ }
}
$scope.newMeasure = null;
@@ -504,6 +512,43 @@
}
}
+ $scope.isNewMeasure = function(measure) {
+ for (var i = 0; i < $scope.newMeasures.length; i++) {
+ if ($scope.newMeasures[i].name === measure.name) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ $scope.measureTimeRange = {};
+ $scope.$watch('cubeMetaFrame.name', function(newValue, oldValue) {
+ if (newValue) {
+ MeasureService.get({segment: "segment", cube: $scope.cubeMetaFrame.name}, {}, function (result) {
+ $scope.measureTimeRange = result;
+ }, function(e) {
+ if (e.data && e.data.exception) {
+ var message = e.data.exception;
+ var msg = !!(message) ? message : 'Failed to take action.';
+ SweetAlert.swal('Oops...', msg, 'error');
+ } else {
+ SweetAlert.swal('Oops...', "Failed to take action.", 'error');
+ }
+ });
+ }
+ })
+
+ $scope.getTimeRange = function(measureName) {
+ var tr = $scope.measureTimeRange[measureName];
+ if (tr) {
+ return "<div style='text-align:left'>"
+ + "Segment Count: " + tr.segment_count + "<br/>"
+ + "Time Range: <br/>" + tr.time_ranges.join("<br/>")
+ + "</div>";
+ } else {
+ return "empty";
+ }
+ }
});
var NextParameterModalCtrl = function ($scope, scope,para,$modalInstance,cubeConfig, CubeService, MessageService, $location, SweetAlert,ProjectModel, loadingRequest,ModelService) {
diff --git a/webapp/app/js/controllers/cubeSchema.js b/webapp/app/js/controllers/cubeSchema.js
index 31a5afe..b77b5b8 100755
--- a/webapp/app/js/controllers/cubeSchema.js
+++ b/webapp/app/js/controllers/cubeSchema.js
@@ -37,6 +37,20 @@
$scope.curStep = $scope.wizardSteps[0];
+ $scope.findMeasuresStepIndex = function() {
+ for (var i = 0; i < $scope.wizardSteps.length; i++) {
+ if ($scope.wizardSteps[i].title === 'Measures') {
+ return i;
+ }
+ }
+ return 0;
+ }
+
+ if ($scope.isMeasureEdit) {
+ var editMeasureStepIdx = $scope.findMeasuresStepIndex();
+ $scope.curStep = $scope.wizardSteps[editMeasureStepIdx];
+ }
+
$scope.getTypeVersion=function(typename){
var searchResult=/\[v(\d+)\]/.exec(typename);
if(searchResult&&searchResult.length){
@@ -388,12 +402,13 @@
}
var cfMeasures = [];
- angular.forEach($scope.cubeMetaFrame.hbase_mapping.column_family,function(cf){
- angular.forEach(cf.columns[0].measure_refs, function (measure, index) {
- cfMeasures.push(measure);
+ angular.forEach($scope.cubeMetaFrame.hbase_mapping.column_family, function (colFamily, index) {
+ angular.forEach(colFamily.columns, function (column, index) {
+ angular.forEach(column.measure_refs, function(measure) {
+ cfMeasures.push(measure);
+ });
});
});
-
var uniqCfMeasures = _.uniq(cfMeasures);
if(uniqCfMeasures.length != $scope.cubeMetaFrame.measures.length) {
errors.push("All measures need to be assigned to column family");
@@ -497,4 +512,8 @@
});
});
}
+
+ $scope.cancel = function () {
+ $location.path("models");
+ }
});
diff --git a/webapp/app/js/controllers/cubes.js b/webapp/app/js/controllers/cubes.js
index d4c67bb..f8eee6b 100644
--- a/webapp/app/js/controllers/cubes.js
+++ b/webapp/app/js/controllers/cubes.js
@@ -606,6 +606,10 @@
});
};
+ $scope.addMeasure = function (cube) {
+ $location.path("cubes/measure/" + cube.name);
+ };
+
$scope.listCubeAccess = function (cube) {
//check project auth for user
$scope.cubeProjectEntity = _.find($scope.projectModel.projects, function(project) {return project.name == $scope.projectModel.selectedProject;});
diff --git a/webapp/app/js/controllers/query.js b/webapp/app/js/controllers/query.js
index 5be4cd7..cb5af5a 100644
--- a/webapp/app/js/controllers/query.js
+++ b/webapp/app/js/controllers/query.js
@@ -569,4 +569,19 @@
$scope.chart.data = [];
}
}
+
+ $scope.getMissInfo = function(query) {
+ if (query.status != 'success') {
+ return '';
+ }
+ var missMeasureInfo = query.result.missMeasureMessage;
+ if (missMeasureInfo) {
+ var retMsg = "";
+ for (var i = 0; i < missMeasureInfo.length; i++) {
+ retMsg += (i+1) + ". " + missMeasureInfo[i] + "\n";
+ }
+ return retMsg;
+ }
+ return "empty";
+ }
});
diff --git a/webapp/app/js/services/measure.js b/webapp/app/js/services/measure.js
new file mode 100644
index 0000000..5429852
--- /dev/null
+++ b/webapp/app/js/services/measure.js
@@ -0,0 +1,23 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements. See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership. The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License. You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+KylinApp.factory('MeasureService', ['$resource', function ($resource, config) {
+ return $resource(Config.service.url + 'measure/:segment/:cube/:segment_name', {}, {
+ get: {method: 'GET', params: {}, isArray: false}
+ });
+}])
diff --git a/webapp/app/partials/cubeDesigner/advanced_settings.html b/webapp/app/partials/cubeDesigner/advanced_settings.html
index 5b063ef..0eae9c9 100755
--- a/webapp/app/partials/cubeDesigner/advanced_settings.html
+++ b/webapp/app/partials/cubeDesigner/advanced_settings.html
@@ -607,7 +607,7 @@
<div style="margin-left:42px">
<div class="box-body">
<!-- VIEW MODE -->
- <div class="row" ng-if="state.mode=='view'&& cubeMetaFrame.hbase_mapping.column_family.length > 0">
+ <div class="row" ng-if="state.mode=='view' || instance.segments.length > 0 && cubeMetaFrame.hbase_mapping.column_family.length > 0">
<table class="table table-striped table-hover">
<thead>
<tr>
@@ -623,7 +623,7 @@
</td>
<!--Name -->
<td class="col-xs-11">
- <span>{{colFamily.columns[0].measure_refs}}</span>
+ <span>{{getCFDisplayString(colFamily)}}</span>
</td>
</tr>
</tbody>
@@ -631,7 +631,7 @@
</div>
<!-- EDIT MODE -->
- <div ng-if="state.mode=='edit'" class="form-group " style="width: 100%">
+ <div ng-if="state.mode=='edit' && instance.segments.length == 0" class="form-group " style="width: 100%">
<table ng-if="cubeMetaFrame.hbase_mapping.column_family.length > 0"
class="table table-hover">
@@ -673,7 +673,7 @@
</div>
<div class="form-group" >
- <button class="btn btn-sm btn-info" ng-click="addColumnFamily()" ng-show="state.mode=='edit'">
+ <button class="btn btn-sm btn-info" ng-click="addColumnFamily()" ng-show="state.mode=='edit' && instance.segments.length == 0">
<i class="fa fa-plus"></i> ColumnFamily
</button>
</div>
diff --git a/webapp/app/partials/cubeDesigner/measures.html b/webapp/app/partials/cubeDesigner/measures.html
index 2e11fe3..c9397df 100755
--- a/webapp/app/partials/cubeDesigner/measures.html
+++ b/webapp/app/partials/cubeDesigner/measures.html
@@ -41,7 +41,7 @@
<tr ng-repeat="measure in cubeMetaFrame.measures | filter: state.measureFilter track by $index">
<td>
<!--Name -->
- <span tooltip="measure name..">{{measure.name}}</span>
+ <span tooltip-html-unsafe="{{getTimeRange(measure.name)}}">{{measure.name}}</span>
</td>
<td>
<!--Expression -->
@@ -63,11 +63,13 @@
</td>
<td ng-if="state.mode=='edit'">
<!--Edit Button -->
- <button class="btn btn-xs btn-info" ng-click="addNewMeasure(measure, $index)" ng-disabled="instance.status=='READY'">
+ <!-- In cubeMode 'editExistCube', user shouldn't edit or remove measure if there still have segments under cube -->
+ <!-- User can only add new measures by click Add Measure in cube list -->
+ <button class="btn btn-xs btn-info" ng-click="addNewMeasure(measure, $index)" ng-disabled="instance.status=='READY' || (!isMeasureEdit && instance.segments.length > 0)" ng-if="!isMeasureEdit || isNewMeasure(measure)">
<i class="fa fa-pencil"></i>
</button>
<!--Remove Button -->
- <button class="btn btn-xs btn-danger" ng-click="removeElement(cubeMetaFrame.measures, measure)" ng-disabled="instance.status=='READY'">
+ <button class="btn btn-xs btn-danger" ng-click="removeElement(cubeMetaFrame.measures, measure);removeElement(newMeasures, measure)" ng-disabled="instance.status=='READY' || (!isMeasureEdit && instance.segments.length > 0)" ng-if="!isMeasureEdit || isNewMeasure(measure)">
<i class="fa fa-trash-o"></i>
</button>
</td>
@@ -78,7 +80,7 @@
</ng-form>
<!--Add Measures Button-->
<div class="form-group">
- <button class="btn btn-sm btn-info" ng-click="addNewMeasure()" ng-show="state.mode=='edit' && !newMeasure" ng-disabled="instance.status=='READY'">
+ <button class="btn btn-sm btn-info" ng-click="addNewMeasure()" ng-show="state.mode=='edit' && !newMeasure" ng-disabled="instance.status=='READY' || (!isMeasureEdit && instance.segments.length > 0)">
<i class="fa fa-plus"></i> Measure
</button>
</div>
diff --git a/webapp/app/partials/cubes/cube_detail.html b/webapp/app/partials/cubes/cube_detail.html
index 8099845..dae5502 100755
--- a/webapp/app/partials/cubes/cube_detail.html
+++ b/webapp/app/partials/cubes/cube_detail.html
@@ -107,7 +107,7 @@
<h5><b>Segment Number:</b> <span class="red">{{cube.hbase.length}}</span> <b>Total Size:</b> <span class="red">{{cube.totalSize | bytes}}</span></h5>
</div>
<div ng-repeat="table in cube.hbase">
- <h5><b>Segment:</b> {{table.segmentName}}</h5>
+ <h5><b>Segment:</b> <span tooltip-html-unsafe="<div style='text-align:left'>Measure Count: {{table.measuresOnSegment.length}}<br/>Measures:<br/>{{table.measuresOnSegment.join('<br/>')}}</div>">{{table.segmentName}}</span></h5>
<ul>
<li ng-if="cube.streaming">Status: <span class="red">{{table.segmentStatus}}</span></li>
<li ng-if="cube.model.partition_desc.partition_date_column">Start Time: <span class="red">{{table.dateRangeStart | reverseToGMT0}}</span></li>
@@ -293,4 +293,4 @@
<div class="col-md-7">{{receiverStats.consume_lag}}</div>
</div>
</div>
-</script>
\ No newline at end of file
+</script>
diff --git a/webapp/app/partials/cubes/cube_schema.html b/webapp/app/partials/cubes/cube_schema.html
index 5cf39dc..0285819 100644
--- a/webapp/app/partials/cubes/cube_schema.html
+++ b/webapp/app/partials/cubes/cube_schema.html
@@ -18,10 +18,11 @@
<div class="box box-primary box-2px">
<div class="box-header widget-header-blue widget-header-flat">
- <h4 class="box-title text-info">Cube Designer</h4>
+ <h4 class="box-title text-info" ng-if="!isMeasureEdit">Cube Designer</h4>
+ <h4 class="box-title text-info" ng-if="isMeasureEdit">Measure Editor</h4>
</div>
<div class="box-body">
- <div>
+ <div ng-if="!isMeasureEdit">
<ul class="wizard-steps">
<li ng-repeat="step in wizardSteps"
class="{{step==curStep?'active':''}} {{step.isComplete?'complete':''}}">
@@ -42,17 +43,21 @@
</div>
</div>
<div class="col-xs-4">
- <button class="btn btn-prev" ng-click="preView()" ng-show="curStep.title!='Cube Info'">
+ <button class="btn btn-prev" ng-click="preView()" ng-show="!isMeasureEdit && curStep.title!='Cube Info'">
<i class="ace-icon fa fa-arrow-left"></i>
Prev
</button>
+ <button class="btn btn-prev" ng-click="cancel()" ng-if="isMeasureEdit">
+ <i class="ace-icon fa fa-arrow-left"></i>
+ Cancel
+ </button>
<button id="nextButton" class="btn btn-success btn-next" ng-click="checkCubeForm($index)?nextView():''" ng-disabled="forms[curStep.form].$invalid"
- ng-show="curStep.title!='Overview'">
+ ng-show="!isMeasureEdit && curStep.title!='Overview'">
Next
<i class="ace-icon fa fa-arrow-right icon-on-right"></i>
</button>
<button class="btn btn-primary" ng-click="prepareCube();saveCube()" ng-disabled="design_form.$invalid"
- ng-if="curStep.title=='Overview' && state.mode=='edit'">
+ ng-if="(isMeasureEdit || curStep.title=='Overview') && state.mode=='edit'">
Save
</button>
</div>
diff --git a/webapp/app/partials/cubes/cubes.html b/webapp/app/partials/cubes/cubes.html
index fd176f7..798ae67 100644
--- a/webapp/app/partials/cubes/cubes.html
+++ b/webapp/app/partials/cubes/cubes.html
@@ -103,7 +103,7 @@
<li ng-if="cube.status=='DISABLED' && (userService.hasRole('ROLE_ADMIN') || hasPermission('cube',cube, permissions.ADMINISTRATION.mask, permissions.MANAGEMENT.mask))"><a ng-click="purge(cube)">Purge</a></li>
<li ng-if="cube.status!='DESCBROKEN' && (userService.hasRole('ROLE_ADMIN') || hasPermission('cube',cube, permissions.ADMINISTRATION.mask, permissions.MANAGEMENT.mask))"><a ng-click="cloneCube(cube)">Clone</a></li>
<li ng-if="cube.status=='READY' && isAutoMigrateCubeEnabled() && (userService.hasRole('ROLE_ADMIN') || hasPermission('cube',cube, permissions.ADMINISTRATION.mask, permissions.MANAGEMENT.mask)) "><a ng-click="migrateCube(cube)">Migrate</a></li>
-
+ <li ng-if="cube.can_add_measure && (userService.hasRole('ROLE_ADMIN') || hasPermission('cube',cube, permissions.ADMINISTRATION.mask, permissions.MANAGEMENT.mask))"><a ng-click="addMeasure(cube)">Add Measure</a></li>
</ul>
<ul ng-if="(userService.hasRole('ROLE_ADMIN') || hasPermission('cube', cube, permissions.ADMINISTRATION.mask, permissions.MANAGEMENT.mask, permissions.OPERATION.mask)) && cube.streamingV2 && actionLoaded" class="dropdown-menu" role="menu" style="right:0;left:auto;">
<li ng-if="cube.status!='DISABLED' && cube.consumeState=='RUNNING'"><a ng-click="pauseCube(cube, $index);">Pause</a></li>
diff --git a/webapp/app/partials/query/query_detail.html b/webapp/app/partials/query/query_detail.html
index 63cf5ac..9add54c 100644
--- a/webapp/app/partials/query/query_detail.html
+++ b/webapp/app/partials/query/query_detail.html
@@ -59,14 +59,18 @@
<span class="label label-lg label-info" ng-if="curQuery.status=='executing'">
<i class="fa fa-cog fa-spin"></i> Executing...</span>
</li>
- <li class="col-md-5 " style="display: inline">
+ <li class="col-md-2 " style="display: inline">
<label>Project: </label>
<span>{{curQuery.project}}</span>
</li>
- <li class="col-md-5 " style="display: inline">
+ <li class="col-md-4 " style="display: inline">
<label>Cubes: </label>
<span>{{curQuery.result.cube | limitTo:30}}<span ng-if="curQuery.result.cube.length > 30">... <i class="fa fa-list text-aqua" style="cursor: pointer;" popover-placement="left" popover="{{curQuery.result.cube | formatCubeName}}" popover-title="Cube Info Details"></i></span></span>
</li>
+ <li class="col-md-4 " style="display: inline">
+ <label>Miss: </label>
+ <span>{{getMissInfo(curQuery) | limitTo:30}}<span ng-if="getMissInfo(curQuery).length > 0">... <i class="fa fa-list text-aqua" style="cursor: pointer;" popover-placement="left" popover="{{getMissInfo(curQuery)}}" popover-title="Miss Info Details"></i></span></span>
+ </li>
</ul>
</div>
diff --git a/webapp/app/routes.json b/webapp/app/routes.json
index 210fda2..2dcddcc 100644
--- a/webapp/app/routes.json
+++ b/webapp/app/routes.json
@@ -48,6 +48,14 @@
}
},
{
+ "url": "/cubes/measure/:cubeName",
+ "params": {
+ "templateUrl": "partials/cubes/cube_edit.html",
+ "tab": "models",
+ "controller": "CubeEditCtrl"
+ }
+ },
+ {
"url": "/sourceMeta",
"params": {
"templateUrl": "partials/tables/source_metadata.html",