This is an automated email from the ASF dual-hosted git repository.

xxyu pushed a commit to branch kylin5
in repository https://gitbox.apache.org/repos/asf/kylin.git

commit 424bb2450e0a942df00c95b89c29a8783d7029c1
Author: Qian Xia <lauraxiaq...@gmail.com>
AuthorDate: Fri Jun 2 17:39:33 2023 +0800

    KYLIN-5541 support aggregate index text recognize
---
 kystudio/build/webpack.base.conf.js                |   2 +-
 kystudio/package.json                              |   1 +
 kystudio/src/assets/styles/index.less              |   1 +
 kystudio/src/components/common/Modal/Modal.vue     |   5 +-
 .../RecognizeAggregateModal.vue                    | 575 +++++++++++++++++++++
 .../common/RecognizeAggregateModal/error.svg       |   3 +
 .../common/RecognizeAggregateModal/handler.js      | 115 +++++
 .../common/RecognizeAggregateModal/locales.js      |  28 +
 .../common/RecognizeAggregateModal/store.js        | 120 +++++
 .../common/RecognizeAggregateModal/warning.svg     |   3 +
 .../StudioModel/ModelList/AggregateModal/index.vue |  47 ++
 .../ModelList/AggregateModal/locales.js            |   3 +-
 kystudio/src/config/index.js                       |   7 +
 13 files changed, 907 insertions(+), 3 deletions(-)

diff --git a/kystudio/build/webpack.base.conf.js 
b/kystudio/build/webpack.base.conf.js
index a1532d7b9b..712ff5c1a3 100644
--- a/kystudio/build/webpack.base.conf.js
+++ b/kystudio/build/webpack.base.conf.js
@@ -55,7 +55,7 @@ module.exports = {
       {
         test: /\.js$/,
         loader: 'babel-loader',
-        include: [resolve('src'), resolve('test'), 
resolve('node_modules/vue-awesome')],
+        include: [resolve('src'), resolve('test'), 
resolve('node_modules/vue-awesome'), 
resolve('node_modules/vue-virtual-scroller')],
         options: {
           presets: ['@babel/preset-env']
         }
diff --git a/kystudio/package.json b/kystudio/package.json
index 98802c940d..d7ed33f2e8 100644
--- a/kystudio/package.json
+++ b/kystudio/package.json
@@ -42,6 +42,7 @@
     "vue-property-decorator": "7.0.0",
     "vue-resource": "1.5.1",
     "vue-router": "2.8.1",
+    "vue-virtual-scroller": "^1.0.10",
     "vue2-ace-editor": "0.0.3",
     "vuex": "2.5.0"
   },
diff --git a/kystudio/src/assets/styles/index.less 
b/kystudio/src/assets/styles/index.less
index 5b1f4c7b0b..1ed2b24a57 100644
--- a/kystudio/src/assets/styles/index.less
+++ b/kystudio/src/assets/styles/index.less
@@ -2,6 +2,7 @@
 @import '~kyligence-kylin-ui/lib/theme-chalk/index.css';
 @import '~smooth-scrollbar/dist/smooth-scrollbar.css';
 @import '~nprogress/nprogress.css';
+@import '~vue-virtual-scroller/dist/vue-virtual-scroller.css';
 
 // custom stylesheet
 @import './icons.less';
diff --git a/kystudio/src/components/common/Modal/Modal.vue 
b/kystudio/src/components/common/Modal/Modal.vue
index a59d797a6a..cbc9978a48 100644
--- a/kystudio/src/components/common/Modal/Modal.vue
+++ b/kystudio/src/components/common/Modal/Modal.vue
@@ -8,6 +8,7 @@
     <ModelsImportModal />
     <ModelERDiagramModal />
     <EditProjectConfigDialog />
+    <RecognizeAggregateModal />
   </div>
 </template>
 
@@ -23,6 +24,7 @@ import ModelsExportModal from 
'../ModelsExportModal/ModelsExportModal.vue'
 import ModelsImportModal from '../ModelsImportModal/ModelsImportModal.vue'
 import ModelERDiagramModal from '../ModelERDiagramModal/ModelERDiagramModal'
 import EditProjectConfigDialog from '../EditProjectConfigDialog'
+import RecognizeAggregateModal from 
'../RecognizeAggregateModal/RecognizeAggregateModal'
 
 @Component({
   computed: {
@@ -38,7 +40,8 @@ import EditProjectConfigDialog from 
'../EditProjectConfigDialog'
     ModelsImportModal,
     ProjectEditModal,
     ModelERDiagramModal,
-    EditProjectConfigDialog
+    EditProjectConfigDialog,
+    RecognizeAggregateModal
   }
 })
 export default class Modal extends Vue {
diff --git 
a/kystudio/src/components/common/RecognizeAggregateModal/RecognizeAggregateModal.vue
 
b/kystudio/src/components/common/RecognizeAggregateModal/RecognizeAggregateModal.vue
new file mode 100644
index 0000000000..c3ab6ac753
--- /dev/null
+++ 
b/kystudio/src/components/common/RecognizeAggregateModal/RecognizeAggregateModal.vue
@@ -0,0 +1,575 @@
+<template>
+    <el-dialog class="recognize-aggregate-modal" width="960px"
+      append-to-body
+      :title="$t('title')"
+      :visible="isShow"
+      :close-on-press-escape="false"
+      :close-on-click-modal="false"
+      :before-close="handleCancel"
+      @closed="handleClosed"
+    >
+      <div class="dialog-content">
+        <div class="recognize-area">
+          <div class="recognize-header" v-if="errorLines.length">
+            <div class="result-counter">
+              <span class="error">{{$tc('errorCount', errorCount, { count: 
errorCount })}}</span>
+              <el-tooltip :content="$t('repeatTip')" placement="top">
+                <span class="warning">{{$tc('repeatCount', repeatCount, { 
count: repeatCount })}}</span>
+              </el-tooltip>
+            </div>
+            <div class="result-actions">
+              <el-button icon-button text type="primary" size="mini" 
icon="el-ksd-n-icon-arrow-up-outlined" @click="handlePrevious" /><!--
+           --><el-button icon-button text type="primary" size="mini" 
icon="el-ksd-n-icon-arrow-down-outlined" @click="handleNext" />
+            </div>
+          </div>
+          <AceEditor :key="isShow" :placeholder="'123'" class="text-input" 
ref="editorRef" :value="form.text" @input="handleInputText" />
+          <div class="actions">
+            <el-tooltip :content="$t('dexecute')" placement="left">
+              <el-button icon-button class="recognize" size="small" 
type="primary" icon="el-icon-caret-right" :disabled="!form.text" 
@click="handleRecognize" />
+            </el-tooltip>
+          </div>
+        </div>
+        <div class="recognize-results">
+          <template v-if="form.dimensions.length">
+            <div class="results-header">
+              {{$tc('selectedDimensionCount', selectedDimensionCount, { count: 
selectedDimensionCount })}}
+            </div>
+            <div class="list-actions">
+              <el-checkbox :key="isSelectAll" :indeterminate="isIndeterminate" 
:checked="isSelectAll" @change="handleSelectAll" />
+              <div class="header-dimension-name">{{$t('dimensionName')}}</div>
+              <div class="header-data-type">{{$t('dataType')}}</div>
+            </div>
+            <RecycleScroller
+              class="dimension-list"
+              :items="form.dimensions"
+              :item-size="37"
+              key-field="value"
+            >
+              <template slot-scope="{ item }">
+                <div class="dimension" @click="handleCheckDimension(item)">
+                  <el-checkbox :key="item.isChecked" :checked="item.isChecked" 
@change="handleCheckDimension(item)" />
+                  <span class="name">{{ item.label }}</span>
+                  <span class="data-type">{{ item.dataType }}</span>
+                  <div v-if="item.isDisabled" class="current-used-mask" />
+                </div>
+              </template>
+            </RecycleScroller>
+          </template>
+          <div class="all-dimension-error" v-else-if="isAllDimensionError">
+            <i class="el-ksd-n-icon-error-circle-filled" />
+            <span>{{$t('recognizeFailed')}}</span>
+          </div>
+          <EmptyData v-else :showImage="false" :content="$t('emptyText')" />
+        </div>
+      </div>
+      <div slot="footer" class="dialog-footer ky-no-br-space">
+        <el-button size="medium" @click="handleCancel">
+          {{$t('kylinLang.common.cancel')}}
+        </el-button>
+        <el-button type="primary" size="medium" 
:disabled="!selectedDimensionCount" @click="handleSubmit">
+          {{$t('kylinLang.common.submit')}}
+        </el-button>
+      </div>
+    </el-dialog>
+  </template>
+  
+  <script>
+  import Vue from 'vue'
+  import AceEditor from 'vue2-ace-editor'
+  import { Component, Watch } from 'vue-property-decorator'
+  import { RecycleScroller } from 'vue-virtual-scroller'
+  import { mapState, mapMutations, mapGetters } from 'vuex'
+  import locales from './locales'
+  import vuex from '../../../store'
+  import EmptyData from '../../common/EmptyData/EmptyData'
+  import store, { types, getInitialErrors, ALERT_STATUS } from './store'
+  import { collectErrorsInEditor, refreshEditor, scrollToLineAndHighlight, 
updatePlaceHolder, ERROR_TYPE } from './handler'
+  import { AGGREGATE_TYPE } from '../../../config'
+  vuex.registerModule(['modals', 'RecognizeAggregateModal'], store)
+  @Component({
+    components: {
+      AceEditor,
+      EmptyData,
+      RecycleScroller
+    },
+    computed: {
+      ...mapState('RecognizeAggregateModal', {
+        isShow: state => state.isShow,
+        type: state => state.type,
+        status: state => state.status,
+        form: state => state.form,
+        errors: state => state.errors,
+        errorLines: state => state.errorLines,
+        errorInEditor: state => state.errorInEditor,
+        errorCursor: state => state.errorCursor,
+        callback: state => state.callback
+      }),
+      ...mapGetters('RecognizeAggregateModal', [
+        'modelDimensions',
+        'includes',
+        'mandatories',
+        'hierarchies',
+        'hierarchyItems',
+        'joints',
+        'jointItems'
+      ])
+    },
+    methods: {
+      ...mapMutations('RecognizeAggregateModal', {
+        setModal: types.SET_MODAL,
+        hideModal: types.HIDE_MODAL,
+        resetModal: types.RESET_MODAL,
+        setModalForm: types.SET_MODAL_FORM
+      })
+    },
+    locales
+  })
+  export default class RecognizeAggregateModal extends Vue {
+    ALERT_STATUS = ALERT_STATUS
+    @Watch('$lang')
+    onLocaleChanged () {
+      this.updatePlaceHolder()
+    }
+    @Watch('isShow')
+    onIsShowChanged (newVal, oldVal) {
+      this.updatePlaceHolder()
+      this.updateRecognizeShortcut(newVal, oldVal)
+    }
+    get errorCount () {
+      const { errorInEditor } = this
+      return errorInEditor.filter(line => [ERROR_TYPE.COLUMN_NOT_IN_MODEL, 
ERROR_TYPE.COLUMN_NOT_IN_INCLUDES].includes(line.type)).length
+    }
+    get repeatCount () {
+      const { errorInEditor } = this
+      return errorInEditor.filter(line => 
[ERROR_TYPE.COLUMN_DUPLICATE].includes(line.type)).length
+    }
+    get selectedDimensionCount () {
+      const { form: { dimensions } } = this
+      return dimensions.filter(dimension => dimension.isChecked).length
+    }
+    get isSelectAll () {
+      const { form: { dimensions } } = this
+      return !dimensions.some(dimension => !dimension.isChecked && 
!dimension.isDisabled)
+    }
+    get isAllDimensionError () {
+      const { selectedDimensionCount, errorCount, repeatCount } = this
+      return !selectedDimensionCount && !!(errorCount + repeatCount)
+    }
+    get isIndeterminate () {
+      const { selectedDimensionCount, isSelectAll } = this
+      return selectedDimensionCount && !isSelectAll
+    }
+    isColumnUsedInCurrent (column) {
+      const { type, includes, mandatories, hierarchyItems, jointItems } = this
+      switch (type) {
+        case AGGREGATE_TYPE.INCLUDE: return includes.includes(column)
+        case AGGREGATE_TYPE.MANDATORY: return mandatories.includes(column)
+        case AGGREGATE_TYPE.HIERARCHY: return hierarchyItems.includes(column)
+        case AGGREGATE_TYPE.JOINT: return jointItems.includes(column)
+        default: return false
+      }
+    }
+    isColumnUsedInOther (column) {
+      const { type, groupIdx, mandatories, hierarchies, joints } = this
+      switch (type) {
+        // 层级和联合中有此维度
+        case AGGREGATE_TYPE.MANDATORY:
+          return hierarchies.some(hierarchy => hierarchy.items.some(item => 
item === column)) ||
+            joints.some(joint => joint.items.some(item => item === column))
+        // 必需、其他层级和联合中有此维度
+        case AGGREGATE_TYPE.HIERARCHY:
+          return mandatories.some(mandatory => mandatory === column) ||
+            hierarchies.some(hierarchy => hierarchy.items.some((item, idx) => 
idx !== groupIdx && item === column)) ||
+            joints.some(joint => joint.items.some(item => item === column))
+        // 必需、层级和其他联合中有此维度
+        case AGGREGATE_TYPE.JOINT:
+          return mandatories.some(mandatory => mandatory === column) ||
+            hierarchies.some(hierarchy => hierarchy.items.some(item => item 
=== column)) ||
+            joints.some(joint => joint.items.some((item, idx) => idx !== 
groupIdx && item === column))
+        // 包含维度不做判断
+        case AGGREGATE_TYPE.INCLUDE:
+        default: return false
+      }
+    }
+    isColumnInModel (column) {
+      const { modelDimensions } = this
+      return modelDimensions.some(d => d.column === column)
+    }
+    isColumnInIncludes (column) {
+      const { includes } = this
+      return includes.includes(column)
+    }
+    getColumnErrorMessage (errorType, column) {
+      switch (errorType) {
+        case ERROR_TYPE.COLUMN_NOT_IN_MODEL:
+          return this.$t('columnNotInModel', { column })
+        case ERROR_TYPE.COLUMN_NOT_IN_INCLUDES:
+          return this.$t('columnNotInIncludes', { column })
+        case ERROR_TYPE.COLUMN_DUPLICATE:
+          return this.$t('columnDuplicate', { column })
+        default: return 'Unknow Error'
+      }
+    }
+    setNotInModelError (column) {
+      const { errors } = this
+      if (!errors.notInModel.includes(column)) this.setModal({ errors: { 
...errors, notInModel: [...errors.notInModel, column] } })
+    }
+    setNotInIncludesError (column) {
+      const { errors } = this
+      if (!errors.notInIncludes.includes(column)) this.setModal({ errors: { 
...errors, notInIncludes: [...errors.notInIncludes, column] } })
+    }
+    setDuplicateError (column) {
+      const { errors } = this
+      if (!errors.duplicate.includes(column)) this.setModal({ errors: { 
...errors, duplicate: [...errors.duplicate, column] } })
+    }
+    setUsedInOthersError (column) {
+      const { errors } = this
+      if (!errors.usedInOthers.includes(column)) this.setModal({ errors: { 
...errors, usedInOthers: [...errors.usedInOthers, column] } })
+    }
+    clearupErrors () {
+      this.setModal({ errors: getInitialErrors() })
+    }
+    updateRecognizeShortcut (newVal, oldVal) {
+      if (!oldVal && newVal) {
+        document.addEventListener('keydown', this.handleDexecute)
+      } else if (!newVal && oldVal) {
+        document.removeEventListener('keydown', this.handleDexecute)
+      }
+    }
+    updatePlaceHolder () {
+      this.$nextTick(() => {
+        const { editorRef } = this.$refs
+        const { editor } = editorRef || {}
+        updatePlaceHolder(editor, (h) => (
+          <div class="ace_placeholder">
+            <div>
+              {this.$t('inputPlaceholder1')}
+              <el-tooltip
+                popperClass="recognize-aggregate-placeholder-tooltip"
+                content={(
+                  <ul>
+                    <li>{this.$t('inputPlaceholderTooltip1')}</li>
+                    <li>{this.$t('inputPlaceholderTooltip2')}</li>
+                  </ul>
+                )}
+                placement="top"
+              >
+                <span 
class="how-to-use">{this.$t('inputPlaceholderTooltipTrigger')}</span>
+              </el-tooltip>
+            </div>
+            <div>
+              {this.$t('inputPlaceholder2')}
+            </div>
+          </div>
+        ))
+      })
+    }
+    showErrors () {
+      const { errors } = this
+      const { editorRef: { editor } } = this.$refs
+      const session = editor.getSession()
+      const { errorInEditor, errorLines } = collectErrorsInEditor(errors, 
editor)
+      session.setAnnotations(errorInEditor.map(error => ({
+        row: error.row,
+        column: 0,
+        text: this.getColumnErrorMessage(error.type, error.column),
+        type: [ERROR_TYPE.COLUMN_DUPLICATE].includes(error.type) ? 'warning' : 
'error'
+      })))
+      this.setModal({ errorLines, errorInEditor })
+      this.$nextTick(() => refreshEditor(editor))
+    }
+    handleDexecute (event) {
+      const { metaKey, key, keyCode } = event
+      if (metaKey && (keyCode === 13 || key === 'Enter')) {
+        this.handleRecognize()
+      }
+    }
+    handleInputText (text) {
+      this.setModalForm({ text })
+    }
+    handleCheckDimension (dimension) {
+      const { form } = this
+      if (!dimension.isDisabled) {
+        const dimensions = form.dimensions.map((d) => (
+          d.value === dimension.value ? { ...d, isChecked: !d.isChecked } : d
+        ))
+        this.setModalForm({ dimensions: [...dimensions] })
+      }
+    }
+    handleRecognize () {
+      const { type, form, modelDimensions } = this
+      const dimensions = []
+      this.clearupErrors()
+      let formattedText = ''
+      for (const text of form.text.replace(/^\n|\n$/g, '').split(/,\n*/g)) {
+        const columnText = text.trim()
+        if (columnText) {
+          const dimension = modelDimensions.find(d => d.column === columnText)
+          if (dimension) {
+            if (type !== AGGREGATE_TYPE.INCLUDE && 
!this.isColumnInIncludes(dimension.column)) {
+              this.setNotInIncludesError(dimension.column)
+            } else if (!this.isColumnUsedInOther(dimension.column)) {
+              const duplicate = dimensions.some(d => d.value === 
dimension.column)
+              if (!duplicate) {
+                const isFormChecked = form.dimensions.find(d => d.value === 
columnText)?.isChecked
+                const isChecked = isFormChecked ?? true
+                const isDisabled = this.isColumnUsedInCurrent(dimension.column)
+                const dataType = dimension.type
+                dimensions.push({ value: dimension.column, label: 
dimension.column, isChecked, isDisabled, dataType })
+              } else {
+                this.setDuplicateError(dimension.column)
+              }
+            } else {
+              this.setUsedInOthersError(dimension.column)
+            }
+          } else {
+            this.setNotInModelError(columnText)
+          }
+          formattedText += `${columnText},\n`
+        }
+      }
+      this.setModalForm({ text: formattedText, dimensions })
+      this.$nextTick(() => {
+        this.showErrors()
+      })
+    }
+    handlePrevious () {
+      const { editorRef: { editor } } = this.$refs
+      const { errorLines, errorCursor } = this
+      const lineIdx = errorLines.indexOf(errorCursor)
+      const nextCursor = errorLines[lineIdx - 1] ?? 
errorLines[errorLines.length - 1]
+      this.setModal({ errorCursor: nextCursor })
+      scrollToLineAndHighlight(editor, nextCursor)
+    }
+    handleNext () {
+      const { editorRef: { editor } } = this.$refs
+      const { errorLines, errorCursor } = this
+      const lineIdx = errorLines.indexOf(errorCursor)
+      const nextCursor = errorLines[lineIdx + 1] ?? errorLines[0]
+      this.setModal({ errorCursor: nextCursor })
+      scrollToLineAndHighlight(editor, nextCursor)
+    }
+    handleSelectAll () {
+      const { form, isSelectAll } = this
+      const dimensions = isSelectAll
+        ? form.dimensions.map((d) => (
+          !d.isDisabled ? { ...d, isChecked: false } : d
+        ))
+        : form.dimensions.map((d) => (
+          !d.isDisabled ? { ...d, isChecked: true } : d
+        ))
+      this.setModalForm({ dimensions })
+    }
+    handleClosed () {
+      this.resetModal()
+    }
+    handleCancel (done) {
+      if (typeof done === 'function') done()
+      this.hideModal()
+    }
+    handleSubmit () {
+      const { form, callback } = this
+      callback(form.dimensions.filter(d => !d.isDisabled && d.isChecked).map(d 
=> d.value))
+      this.hideModal()
+    }
+  }
+  </script>
+  <style lang="less">
+  @import '../../../assets/styles/variables.less';
+  .recognize-aggregate-modal {
+    .el-dialog {
+      width: 800px !important;
+    }
+    .el-dialog__header {
+      padding: 24px 24px 16px 24px;
+    }
+    .el-dialog__body {
+      padding: 0;
+      border-top: 1px solid @ke-border-divider-color;
+      border-bottom: 1px solid @ke-border-divider-color;
+    }
+    .dialog-content {
+      display: flex;
+      height: 424px;
+    }
+    .recognize-area {
+      display: flex;
+      flex-direction: column;
+      width: 30%;
+      position: relative;
+      border-right: 1px solid @ke-border-divider-color;
+    }
+    .recognize-header {
+      height: 38px;
+      flex: none;
+      display: flex;
+      align-items: center;
+      position: relative;
+      padding: 0px 8px;
+      border-bottom: 1px solid @ke-border-divider-color;
+    }
+    .result-counter {
+      font-weight: 400;
+      font-size: 12px;
+      line-height: 16px;
+      .error {
+        color: @ke-color-danger-hover;
+      }
+      .warning {
+        color: @ke-color-warning-hover;
+      }
+      * + * {
+        margin-left: 7px;
+      }
+    }
+    .result-actions {
+      position: absolute;
+      top: 50%;
+      right: 8px;
+      transform: translateY(-50%);
+      .el-button + .el-button {
+        margin-left: 4px;
+      }
+      .is-text:focus:not(:hover) {
+        background: transparent;
+        border-color: transparent;
+      }
+    }
+    .text-input {
+      background-color: @ke-background-color-secondary;
+      .ace_gutter {
+        background-color: @ke-background-color-secondary;
+      }
+      .ace_placeholder {
+        font-weight: 400;
+        font-size: 12px;
+        line-height: 16px;
+        color: @text-normal-color;
+        padding: 0 6px;
+        white-space: pre-wrap;
+      }
+    }
+    .recognize-results {
+      width: 70%;
+      display: flex;
+      flex-direction: column;
+      position: relative;
+      overflow-x: hidden;
+      overflow-y: auto;
+    }
+    .results-header {
+      height: 38px;
+      flex: none;
+      display: flex;
+      align-items: center;
+      position: relative;
+      padding: 0px 8px;
+      border-bottom: 1px solid @ke-border-divider-color;
+      font-weight: 400;
+      font-size: 12px;
+      line-height: 16px;
+    }
+    .actions {
+      position: absolute;
+      right: 16px;
+      bottom: 16px;
+      z-index: 1;
+    }
+    .empty-data {
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+    }
+    .list-actions {
+      display: flex;
+      border-bottom: 1px solid @ke-border-divider-color;
+      align-items: center;
+      font-weight: 500;
+      font-size: 12px;
+      line-height: 16px;
+      padding: 10px;
+      .el-checkbox__inner {
+        display: block;
+      }
+      .el-checkbox {
+        margin-right: 16px;
+      }
+    }
+    .dimension-list {
+      border-radius: 3px;
+      height: 100%;
+      box-sizing: border-box;
+    }
+    .dimension {
+      position: relative;
+      padding: 8px 10px;
+      cursor: pointer;
+      display: flex;
+      align-items: center;
+      border-bottom: 1px solid @ke-border-divider-color;
+      user-select: none;
+      .el-checkbox {
+        margin-right: 16px;
+      }
+      &:hover {
+        background-color: @ke-color-info-secondary-bg;
+      }
+    }
+    .dimension > * {
+      vertical-align: middle;
+    }
+    .header-data-type,
+    .data-type {
+      position: absolute;
+      right: 10px;
+      width: 100px;
+    }
+    .current-used-mask {
+      cursor: not-allowed;
+      background: white;
+      opacity: 0.5;
+      position: absolute;
+      top: 0;
+      right: 0;
+      bottom: 0;
+      left: 0;
+      z-index: 1;
+    }
+    .ace_gutter-cell.ace_error {
+      background-image: url('./error.svg');
+      background-repeat: no-repeat;
+      background-position: 4px center;
+    }
+    .ace_gutter-cell.ace_warning {
+      background-image: url('./warning.svg');
+      background-repeat: no-repeat;
+      background-position: 4px center;
+    }
+    .all-dimension-error {
+      position: absolute;
+      top: 50%;
+      left: 50%;
+      transform: translate(-50%, -50%);
+      color: @ke-color-danger-hover;
+    }
+    .how-to-use {
+      color: @ke-color-primary;
+      cursor: pointer;
+      &:hover {
+        color: @ke-color-primary-hover;
+      }
+    }
+  }
+  .recognize-aggregate-placeholder-tooltip {
+    font-weight: 400;
+    font-size: 12px;
+    line-height: 16px;
+    ul, li {
+      list-style: disc;
+    }
+    ul {
+      margin-left: 15px;
+    }
+  }
+  </style>
+  
\ No newline at end of file
diff --git a/kystudio/src/components/common/RecognizeAggregateModal/error.svg 
b/kystudio/src/components/common/RecognizeAggregateModal/error.svg
new file mode 100644
index 0000000000..b36e44207e
--- /dev/null
+++ b/kystudio/src/components/common/RecognizeAggregateModal/error.svg
@@ -0,0 +1,3 @@
+<svg width="5" height="6" viewBox="0 0 5 6" fill="none" 
xmlns="http://www.w3.org/2000/svg";>
+<circle cx="2.5" cy="3" r="2.5" fill="#E03B3B"/>
+</svg>
diff --git a/kystudio/src/components/common/RecognizeAggregateModal/handler.js 
b/kystudio/src/components/common/RecognizeAggregateModal/handler.js
new file mode 100644
index 0000000000..cf4930ca14
--- /dev/null
+++ b/kystudio/src/components/common/RecognizeAggregateModal/handler.js
@@ -0,0 +1,115 @@
+import { acequire } from 'brace'
+import Vue from 'vue'
+
+const { Range } = acequire('ace/range')
+
+const dom = acequire('ace/lib/dom')
+
+export const ERROR_TYPE = {
+  COLUMN_NOT_IN_MODEL: 'columnNotInModel',
+  COLUMN_NOT_IN_INCLUDES: 'columnNotInIncludes',
+  COLUMN_DUPLICATE: 'columnDuplicate'
+}
+
+export function $updatePlaceholder (editor, renderPlaceholder) {
+  const value = editor.renderer.$composition || editor.getValue()
+  if (value && editor.renderer.placeholderNode) {
+    editor.renderer.off('afterRender', editor.$updatePlaceholder)
+    dom.removeCssClass(editor.container, 'ace_hasPlaceholder')
+    editor.renderer.placeholderNode.remove()
+    editor.renderer.placeholderNode = null
+  } else if (!value && !editor.renderer.placeholderNode) {
+    editor.renderer.on('afterRender', editor.$updatePlaceholder)
+    dom.addCssClass(editor.container, 'ace_hasPlaceholder')
+    var el = dom.createElement('div')
+    editor.renderer.placeholderNode = el
+    editor.renderer.content.appendChild(editor.renderer.placeholderNode)
+
+    var vmEl = dom.createElement('div')
+    el.appendChild(vmEl)
+    editor.renderer.placeholderVm = new Vue({ el: vmEl, render: 
renderPlaceholder })
+  }
+}
+
+export function updatePlaceHolder (editor, renderPlaceholder) {
+  if (editor) {
+    if (!editor.$updatePlaceholder) {
+      editor.$updatePlaceholder = $updatePlaceholder.bind(this, editor, 
renderPlaceholder)
+      editor.on('input', editor.$updatePlaceholder)
+    }
+    editor.$updatePlaceholder(editor, renderPlaceholder)
+  }
+}
+
+export function refreshEditor (editor) {
+  if (editor) {
+    editor.resize(true)
+  }
+}
+
+export function clearupMarkers (editor) {
+  const session = editor.getSession()
+  for (const marker of Object.values(session.getMarkers())) {
+    if (marker.type === 'fullLine') {
+      session.removeMarker(marker.id)
+    }
+  }
+}
+
+export function scrollToLineAndHighlight (editor, line) {
+  const session = editor.getSession()
+  if (line !== undefined) {
+    clearupMarkers(editor)
+    editor.scrollToLine(line, true)
+    const range = new Range(line, 0, line, 1)
+    session.addMarker(range, 'ace_active-line', 'fullLine')
+  }
+}
+
+export function searchColumnInEditor (editor, column) {
+  const { $search: editorSearch } = editor
+  const session = editor.getSession()
+
+  editorSearch.setOptions({
+    needle: `^${column},\n`,
+    caseSensitive: true,
+    wholeWord: false,
+    regExp: true
+  })
+  return editorSearch.findAll(session)
+}
+
+export function collectErrorsInEditor (errors, editor) {
+  const { notInModel, duplicate, notInIncludes } = errors
+
+  let errorInEditor = []
+  let errorLines = []
+
+  for (const column of notInModel) {
+    const notInModelRanges = searchColumnInEditor(editor, column)
+    errorInEditor = [...errorInEditor, ...notInModelRanges.map(r => {
+      errorLines.push(r.start.row)
+      return { row: r.start.row, column, type: ERROR_TYPE.COLUMN_NOT_IN_MODEL }
+    })]
+  }
+
+  for (const column of notInIncludes) {
+    const notInIncludesRanges = searchColumnInEditor(editor, column)
+    errorInEditor = [...errorInEditor, ...notInIncludesRanges.map(r => {
+      errorLines.push(r.start.row)
+      return { row: r.start.row, column, type: 
ERROR_TYPE.COLUMN_NOT_IN_INCLUDES }
+    })]
+  }
+
+  for (const column of duplicate) {
+    const [, ...duplicateRanges] = searchColumnInEditor(editor, column)
+    errorInEditor = [...errorInEditor, ...duplicateRanges.map(r => {
+      errorLines.push(r.start.row)
+      return { row: r.start.row, column, type: ERROR_TYPE.COLUMN_DUPLICATE }
+    })]
+  }
+
+  errorLines = errorLines.sort()
+
+  return { errorInEditor, errorLines }
+}
diff --git a/kystudio/src/components/common/RecognizeAggregateModal/locales.js 
b/kystudio/src/components/common/RecognizeAggregateModal/locales.js
new file mode 100644
index 0000000000..31ebef97e2
--- /dev/null
+++ b/kystudio/src/components/common/RecognizeAggregateModal/locales.js
@@ -0,0 +1,28 @@
+export default {
+  'en': {
+    title: 'Text Recognition',
+    emptyText: 'The recognized columns will be displayed here',
+    previous: 'Prev',
+    next: 'Next',
+    recognize: 'Recognize',
+    errorCount: ' | {count} error | {count} errors',
+    repeatCount: ' | {count} duplicate | {count} duplicates',
+    selectedDimensionCount: 'Select {count} results | Select {count} result | 
Select {count} results',
+    usedDimensionCount: '{count} already exists',
+    inputPlaceholder1: 'Please paste the text, separated by "," to identify 
the selected column.',
+    inputPlaceholder2: 'Example: CUSTOMER.C_CUSTKEY,CUSTOMER.C_CUSTKEY',
+    inputPlaceholderTooltip1: 'Method 1: Enter the formula A1 & "," on a new 
column in Excel, enter and drag the bottom right corner of the cell to add in 
bulk;',
+    inputPlaceholderTooltip2: 'Method 2: Select all the cells that need to 
added in bulk, right-click and select the cell format (shortcut cmd & ctrl + 
1). Select "Custom", enter English format General "," or @ "," in the type, 
confirm and add in bulk.',
+    inputPlaceholderTooltipTrigger: 'Not sure how to batch add characters?',
+    recognizeFailed: 'Recognize failed. No result, please check and try again',
+    columnDuplicate: 'Duplicate with {column}',
+    columnNotInModel: 'Column {column} does not exist in the current model',
+    columnNotInIncludes: 'Column {column} does not exist in include dimension',
+    columnUsedInOther: 'Column {column} is used in other dimension',
+    dimensionName: 'Dimension Name',
+    dataType: 'Data Type',
+    dexecute: 'Dexecute',
+    acceleratorKey: ' ⌃/⌘ enter',
+    repeatTip: 'The selectable options have been automatically deduplicated'
+  }
+}
diff --git a/kystudio/src/components/common/RecognizeAggregateModal/store.js 
b/kystudio/src/components/common/RecognizeAggregateModal/store.js
new file mode 100644
index 0000000000..47ab670b4b
--- /dev/null
+++ b/kystudio/src/components/common/RecognizeAggregateModal/store.js
@@ -0,0 +1,120 @@
+import { AGGREGATE_TYPE } from '../../../config'
+
+const types = {
+  SHOW_MODAL: 'SHOW_MODAL',
+  HIDE_MODAL: 'HIDE_MODAL',
+  SET_MODAL: 'SET_MODAL',
+  RESET_MODAL: 'RESET_MODAL',
+  SET_MODAL_FORM: 'SET_MODAL_FORM',
+  CALL_MODAL: 'CALL_MODAL'
+}
+
+export const ALERT_STATUS = {
+  INIT: 'INIT',
+  SUCCESS: 'SUCCESS',
+  WARNING: 'WARNING',
+  ERROR: 'ERROR'
+}
+
+export function getInitialErrors () {
+  return {
+    notInModel: [],
+    notInIncludes: [],
+    duplicate: [],
+    usedInOthers: []
+  }
+}
+
+function getInitialState () {
+  return {
+    isShow: false,
+    model: null,
+    aggregate: null,
+    type: AGGREGATE_TYPE.INCLUDE,
+    status: ALERT_STATUS.INIT,
+    groupIdx: null,
+    errors: getInitialErrors(),
+    errorLines: [],
+    errorInEditor: [],
+    errorCursor: 0,
+    form: {
+      text: '',
+      // { label, value, isChecked, type }
+      dimensions: []
+    },
+    callback: null
+  }
+}
+
+export default {
+  state: getInitialState(),
+  mutations: {
+    [types.SHOW_MODAL] (state) {
+      state.isShow = true
+    },
+    [types.HIDE_MODAL] (state) {
+      state.isShow = false
+    },
+    [types.SET_MODAL] (state, payload) {
+      for (const [key, value] of Object.entries(payload)) {
+        state[key] = value
+      }
+    },
+    [types.RESET_MODAL] (state) {
+      for (const [key, value] of Object.entries(getInitialState())) {
+        state[key] = value
+      }
+    },
+    [types.SET_MODAL_FORM] (state, payload) {
+      for (const [key, value] of Object.entries(payload)) {
+        state.form[key] = value
+      }
+    }
+  },
+  getters: {
+    includes (state) {
+      const { aggregate } = state
+      return aggregate?.includes ?? []
+    },
+    mandatories (state) {
+      const { aggregate } = state
+      return aggregate?.mandatory ?? []
+    },
+    hierarchies (state) {
+      const { aggregate } = state
+      return aggregate?.hierarchyArray ?? []
+    },
+    joints (state) {
+      const { aggregate } = state
+      return aggregate?.jointArray ?? []
+    },
+    tableIndexCols (state) {
+      const { allColumns } = state
+      return allColumns.filter(c => c.isUsed).map(c => c.fullName)
+    },
+    hierarchyItems (state) {
+      const { aggregate, groupIdx } = state
+      return aggregate?.hierarchyArray[groupIdx]?.items ?? []
+    },
+    jointItems (state) {
+      const { aggregate, groupIdx } = state
+      return aggregate?.jointArray[groupIdx]?.items ?? []
+    },
+    modelDimensions (state) {
+      const { model } = state
+      return model?.simplified_dimensions?.filter(c => c.status === 
'DIMENSION') ?? []
+    }
+  },
+  actions: {
+    [types.CALL_MODAL] ({ commit }, args) {
+      const { aggregate, type, model, allColumns = [], groupIdx = null } = args
+      return new Promise(resolve => {
+        commit(types.SET_MODAL, { aggregate, model, type, groupIdx, 
allColumns, callback: resolve })
+        commit(types.SHOW_MODAL)
+      })
+    }
+  },
+  namespaced: true
+}
+
+export { types }
diff --git a/kystudio/src/components/common/RecognizeAggregateModal/warning.svg 
b/kystudio/src/components/common/RecognizeAggregateModal/warning.svg
new file mode 100644
index 0000000000..7d416355bc
--- /dev/null
+++ b/kystudio/src/components/common/RecognizeAggregateModal/warning.svg
@@ -0,0 +1,3 @@
+<svg width="5" height="6" viewBox="0 0 5 6" fill="none" 
xmlns="http://www.w3.org/2000/svg";>
+<circle cx="2.5" cy="3" r="2.5" fill="#F29D41"/>
+</svg>
diff --git 
a/kystudio/src/components/studio/StudioModel/ModelList/AggregateModal/index.vue 
b/kystudio/src/components/studio/StudioModel/ModelList/AggregateModal/index.vue
index e8fc98a331..7ad21f77b9 100644
--- 
a/kystudio/src/components/studio/StudioModel/ModelList/AggregateModal/index.vue
+++ 
b/kystudio/src/components/studio/StudioModel/ModelList/AggregateModal/index.vue
@@ -94,6 +94,18 @@
                         <div class="ksd-mb-10">
                           <span class="title font-medium include-title"><span 
class="is-required">*</span> {{$t('include')}}</span>
                           <div class="row ksd-fright ky-no-br-space">
+                            <common-tip placement="top" 
:content="(model.model_type === 'HYBRID' && 
!form.aggregateArray[aggregateIdx].index_range) ? $t('disableAddDim') : 
$t('refuseAddIndexTip')"
+                              :disabled="!(model.model_type === 'HYBRID' && 
!form.aggregateArray[aggregateIdx].index_range) || (!indexUpdateEnabled && 
['HYBRID', 'STREAMING'].includes(aggregate.index_range))">
+                              <el-button
+                                plain
+                                class="ksd-ml-10"
+                                size="mini"
+                                :disabled="(model.model_type === 'HYBRID' && 
!form.aggregateArray[aggregateIdx].index_range) || (!indexUpdateEnabled && 
['HYBRID', 'STREAMING'].includes(aggregate.index_range))"
+                                
@click="handleIncludesRecognize(AGGREGATE_TYPE.INCLUDE, aggregateIdx)"
+                              >
+                                {{$t('textRecognition')}}
+                              </el-button>
+                            </common-tip>
                             <common-tip placement="top" 
:content="$t('refuseAddIndexTip')"
                               :disabled="!(!indexUpdateEnabled && ['HYBRID', 
'STREAMING'].includes(aggregate.index_range))">
                               <el-button
@@ -140,6 +152,20 @@
                             <common-tip placement="right" 
:content="$t('mandatoryDesc')">
                               <i class="el-ksd-icon-more_info_16"></i>
                             </common-tip>
+                            <span class="row ksd-fright ky-no-br-space">
+                              <common-tip placement="top" 
v-if="form.aggregateArray[aggregateIdx].includes.length" 
:content="(model.model_type === 'HYBRID' && 
!form.aggregateArray[aggregateIdx].index_range) ? $t('disableAddDim') : 
$t('refuseAddIndexTip')"
+                                :disabled="!(model.model_type === 'HYBRID' && 
!form.aggregateArray[aggregateIdx].index_range) || (!indexUpdateEnabled && 
['HYBRID', 'STREAMING'].includes(aggregate.index_range))">
+                                <el-button
+                                  plain
+                                  class="ksd-ml-10"
+                                  size="mini"
+                                  :disabled="(model.model_type === 'HYBRID' && 
!form.aggregateArray[aggregateIdx].index_range) || (!indexUpdateEnabled && 
['HYBRID', 'STREAMING'].includes(aggregate.index_range))"
+                                  
@click="handleMandatoryRecognize(AGGREGATE_TYPE.MANDATORY, aggregateIdx)"
+                                >
+                                  {{$t('textRecognition')}}
+                                </el-button>
+                              </common-tip>
+                            </span>
                           </h2>
                           <el-select
                             multiple
@@ -541,6 +567,7 @@ import { mapState, mapGetters, mapMutations, mapActions } 
from 'vuex'
 
 import vuex from 'store'
 import locales from './locales'
+import { AGGREGATE_TYPE } from 'config'
 import { BuildIndexStatus } from 'config/model'
 import store, { types, initialAggregateData } from './store'
 import { titleMaps, getPlaintDimensions, findIncludeDimension } from 
'./handler'
@@ -591,6 +618,9 @@ vuex.registerModule(['modals', 'AggregateModal'], store)
     ...mapMutations({
       setChangedForm: 'SET_CHANGED_FORM',
       setProject: 'SET_PROJECT'
+    }),
+    ...mapActions('RecognizeAggregateModal', {
+      callRecognizeAggregateModal: types.CALL_MODAL
     })
   },
   locales
@@ -666,6 +696,7 @@ export default class AggregateModal extends Vue {
       width: [50, 1000]
     }
   }
+  AGGREGATE_TYPE = AGGREGATE_TYPE
 
   @Watch('$lang')
   changeCurrentLang (newVal, oldVal) {
@@ -1095,6 +1126,22 @@ export default class AggregateModal extends Vue {
       }
     })
   }
+  async handleIncludesRecognize (type, aggregateIdx, groupIdx = 0) {
+    const { model, form } = this
+    const { aggregateArray = [] } = form
+    const aggregate = aggregateArray[aggregateIdx]
+    const selectedColumns = await this.callRecognizeAggregateModal({ type, 
model, aggregate, groupIdx })
+    const value = [...aggregate.includes, ...selectedColumns]
+    this.handleInput(`aggregateArray.${aggregateIdx}.includes`, value, 
aggregate.id)
+  }
+  async handleMandatoryRecognize (type, aggregateIdx, groupIdx = 0) {
+    const { model, form } = this
+    const { aggregateArray = [] } = form
+    const aggregate = aggregateArray[aggregateIdx]
+    const selectedColumns = await this.callRecognizeAggregateModal({ type, 
model, aggregate, groupIdx })
+    const value = [...aggregate.mandatory, ...selectedColumns]
+    this.handleInput(`aggregateArray.${aggregateIdx}.mandatory`, value, 
aggregate.id)
+  }
   handleRemoveAllIncludes (aggregateIdx, titleId, id) {
     kylinConfirm(this.$t('clearAllAggregateTip', {aggId: titleId}), {type: 
'warning'}, this.$t('clearAggregateTitle')).then(() => {
       const { aggregateArray = [] } = this.form
diff --git 
a/kystudio/src/components/studio/StudioModel/ModelList/AggregateModal/locales.js
 
b/kystudio/src/components/studio/StudioModel/ModelList/AggregateModal/locales.js
index c48786c10b..af73fae4c2 100644
--- 
a/kystudio/src/components/studio/StudioModel/ModelList/AggregateModal/locales.js
+++ 
b/kystudio/src/components/studio/StudioModel/ModelList/AggregateModal/locales.js
@@ -114,6 +114,7 @@ export default {
     manyToManyAntiTableTip: 'For the tables excluded from recommendations, if 
the join relationship of a table is One-to-Many or Many-to-Many, dimensions 
from this table can\'t be used in indexes. ',
     indexTimeRangeTips: 'The data range that the indexes will be built in. 
With “Batch and Streaming“ selected, there will be generated batch indexes and 
streaming indexes with same content respectively. ',
     refuseAddIndexTip: 'Can\'t add streaming indexes. Please stop the 
streaming job and then delete all the streaming segments.',
-    disableAddDim: 'Select index\'s data range'
+    disableAddDim: 'Select index\'s data range',
+    textRecognition: 'Text Recognition'
   }
 }
diff --git a/kystudio/src/config/index.js b/kystudio/src/config/index.js
index 9b51bf0819..1c9e1da646 100644
--- a/kystudio/src/config/index.js
+++ b/kystudio/src/config/index.js
@@ -730,3 +730,10 @@ export const formatSQLConfig = {
 }
 
 export { projectCfgs } from './projectCfgs'
+
+export const AGGREGATE_TYPE = {
+  INCLUDE: 'includes',
+  MANDATORY: 'mandatory',
+  HIERARCHY: 'hierarchyArray',
+  JOINT: 'jointArray'
+}


Reply via email to