kystudio/src/components/studio/StudioModel/ModelList/ModelBuildModal/build.vue (1,301 lines of code) (raw):

<template> <!-- 模型构建 --> <el-dialog class="model-build" :title="title" width="560px" :visible="isShow" v-if="isShow" :close-on-press-escape="false" :close-on-click-modal="false" :append-to-body="true" @close="isShow && closeModal(false)"> <div> <p class="habird-tips ksd-mb-16" v-if="isHybridModel">{{$t('habirdModelBuildTips')}}</p> <el-alert :title="$t('secondStoragePartitionTips')" type="error" :closable="false" class="ksd-mb-10" v-if="secondStoragePartitionTips" show-icon> </el-alert> <el-alert :title="$t('changeBuildTypeTips')" type="warning" :closable="false" class="ksd-mb-10" v-if="isShowWarning" show-icon> </el-alert> <div class="ksd-title-label-small ksd-mb-10">{{$t('chooseBuildType')}}</div> <el-select v-model="buildType" class="ksd-mb-5" @change="handChangeBuildType" v-if="buildOrComplete == 'build'" :disabled="!datasourceActions.includes('changeBuildType')"> <el-option :label="$t('incremental')" value="incremental"></el-option> <el-option :label="$t('fullLoad')" v-if="!isStreamModel" value="fullLoad"></el-option> </el-select> </div> <div class="tips">{{buildTips}}</div> <div v-if="buildType === 'fullLoad'"> <el-alert class="ksd-pt-0 ksd-mt-15" :title="$t('segmentTips')" type="warning" :show-background="false" :closable="false" show-icon> </el-alert> <span v-if="isHaveSegment">{{fullLoadBuildTips}}</span> <span v-else>{{$t('willAddSegmentTips')}}</span> </div> <div v-if="buildType === 'incremental' && buildOrComplete === 'build'"> <el-form class="ksd-mb-20 ksd-mt-15" v-if="isExpand" :model="partitionMeta" ref="partitionForm" :rules="partitionRules" label-width="85px" label-position="top"> <el-form-item :label="$t('partitionDateColumn')" class="clearfix"> <el-row :gutter="5"> <el-col :span="12"> <el-tooltip effect="dark" :content="$t('disableChangePartitionTips')" :disabled="!isNotBatchModel" placement="bottom"> <el-select :disabled="isLoadingNewRange || !datasourceActions.includes('changePartition') || isNotBatchModel" v-model="partitionMeta.table" @change="partitionTableChange" :placeholder="$t('kylinLang.common.pleaseSelectOrSearch')" style="width:100%"> <el-option :label="t.alias" :value="t.alias" v-for="t in partitionTables" :key="t.alias">{{t.alias}}</el-option> </el-select> </el-tooltip> </el-col> <el-col :span="12" v-if="partitionMeta.table"> <el-form-item prop="column"> <el-tooltip effect="dark" :content="$t('disableChangePartitionTips')" :disabled="!isNotBatchModel" placement="bottom"> <el-select :disabled="isLoadingNewRange || !datasourceActions.includes('changePartition')||isNotBatchModel" @change="partitionColumnChange" v-model="partitionMeta.column" :placeholder="$t('kylinLang.common.pleaseSelectOrSearch')" filterable style="width:100%"> <i slot="prefix" class="el-input__icon el-ksd-icon-search_22" v-if="!partitionMeta.column.length"></i> <el-option :label="t.name" :value="t.name" v-for="t in columns" :key="t.name"> <el-tooltip :content="t.name" effect="dark" placement="top" :disabled="showToolTip(t.name)"><span style="float: left">{{ t.name | omit(15, '...') }}</span></el-tooltip> <span class="ky-option-sub-info">{{ t.datatype.toLocaleLowerCase() }}</span> </el-option> </el-select> </el-tooltip> </el-form-item> </el-col> </el-row> </el-form-item> <el-form-item :label="$t('dateFormat')" :class="{'is-error': errorFormat}" v-if="partitionMeta.table"> <el-row :gutter="5"> <el-col :span="partitionMeta.column&&$store.state.project.projectPushdownConfig&&!isNotBatchModel ? 22 : 24"> <el-tooltip effect="dark" :content="$t('disableChangePartitionTips')" :disabled="!isNotBatchModel" placement="bottom"> <el-select :disabled="isLoadingNewRange || isLoadingFormat || !datasourceActions.includes('changePartition') || isNotBatchModel" style="width:100%" @change="val => partitionColumnFormatChange(val)" v-model="partitionMeta.format" filterable allow-create default-first-option :placeholder="$t('pleaseInputColumn')"> <el-option-group> <el-option v-if="dateFormatsOptions.map(it => it.value).indexOf(prevPartitionMeta.format) === -1 && prevPartitionMeta.format" :label="prevPartitionMeta.format" :value="prevPartitionMeta.format"></el-option> <el-option :label="f.label" :value="f.value" v-for="f in dateFormatsOptions" :key="f.label"></el-option> </el-option-group> </el-select> </el-tooltip> </el-col> <el-col :span="2" v-if="partitionMeta.column&&$store.state.project.projectPushdownConfig&&!isNotBatchModel"> <el-tooltip effect="dark" :content="$t('detectFormat')" placement="top"> <div style="display: inline-block;"> <el-button size="medium" class="auto-detect-btn" :loading="isLoadingFormat" :disabled="isLoadingNewRange || !datasourceActions.includes('changePartition')" icon="el-ksd-icon-data_range_search_old" @click="handleLoadFormat"> </el-button> </div> </el-tooltip> </el-col> </el-row> <div class="error-format" v-if="errorFormat">{{errorFormat}}</div> <div class="pre-format" v-if="formatedDate">{{$t('previewFormat')}}{{formatedDate}}</div> <div class="format">{{$t('formatRule')}} <span v-if="isExpandFormatRule" @click="isExpandFormatRule = false">{{$t('viewDetail')}}<i class="el-icon-ksd-more_01-copy arrow"></i></span> <span v-else @click="isExpandFormatRule = true">{{$t('viewDetail')}}<i class="el-icon-ksd-more_02 arrow"></i></span> </div> <div class="detail-content" v-if="isExpandFormatRule"> <p><span class="ksd-mr-2">1. </span><span>{{$t('rule1')}}</span></p> <p><span class="ksd-mr-2">2. </span><span>{{$t('rule2')}}</span></p> <p><span class="ksd-mr-2">3. </span><span>{{$t('rule3')}}</span></p> </div> <span style="position:absolute;width:1px; height:0" v-if="partitionMeta.format"></span> </el-form-item> <el-form-item v-if="((!modelDesc.multi_partition_desc && $store.state.project.multi_partition_enabled) || modelDesc.multi_partition_desc) && partitionMeta.table && !isNotBatchModel"> <span slot="label"> <span>{{$t('multilevelPartition')}}</span> <el-tooltip effect="dark" :content="$t('multilevelPartitionDesc')" placement="right"> <i class="el-icon-ksd-what"></i> </el-tooltip> </span> <el-row> <el-col :span="11"> <el-select :disabled="isLoadingNewRange || !datasourceActions.includes('changePartition')" v-model="partitionMeta.multiPartition" :placeholder="$t('kylinLang.common.pleaseSelectOrSearch')" filterable class="partition-multi-partition" popper-class="js_multi-partition" style="width:100%" @change="changePartitionSetting" > <i slot="prefix" class="el-input__icon el-ksd-icon-search_22" v-if="!partitionMeta.multiPartition.length"></i> <el-option :label="$t('noPartition')" value=""></el-option> <el-option :label="t.name" :value="t.name" v-for="t in subPartitionColumnsOptions" :key="t.name"> <el-tooltip :content="t.name" effect="dark" placement="top" :disabled="showToolTip(t.name)"><span style="float: left">{{ t.name | omit(15, '...') }}</span></el-tooltip> <span class="ky-option-sub-info">{{ t.datatype.toLocaleLowerCase() }}</span> </el-option> </el-select> </el-col> </el-row> </el-form-item> </el-form> <div v-if="partitionMeta.table && partitionMeta.column && partitionMeta.format"> <div class="divide-block"> <span v-if="isExpand" @click="toggleShowPartition">{{$t('showLess')}}</span> <span v-else @click="toggleShowPartition">{{$t('showMore')}}</span> <div class="divide-line"></div> </div> <div class="ksd-title-label-small ksd-mb-10">{{$t('addRangeTitle')}}</div> <el-form :model="modelBuildMeta" ref="buildForm" :rules="rules" label-position="top"> <el-form-item :class="{'is-error': isShowErrorSegments}" :rule="modelBuildMeta.isLoadExisted ? [] : [{required: true, trigger: 'blur', message: this.$t('dataRangeValValid')}]"> <p class="ksd-pt-0 ksd-fs-12 segment-tips"><i class="el-icon-ksd-alert ksd-mr-5 alert-icon"></i>{{$t('segmentTips')}}</p> <div class="ky-no-br-space" style="height:32px;"> <el-date-picker type="datetime" style="width: 44%;" :class="['ksd-mr-5', {'is-error': dateErrorMsg}]" ref="prevPicker" v-model="modelBuildMeta.dataRangeVal[0]" :disabled="modelBuildMeta.isLoadExisted || isLoadingNewRange" @change="(v) => handleChangeDateTime(v, 'start')" value-format="timestamp" :is-auto-complete="true" :format="partitionFormat" > </el-date-picker> <el-date-picker type="datetime" style="width: 44%;" ref="nextPicker" :class="{'is-error': dateErrorMsg}" v-model="modelBuildMeta.dataRangeVal[1]" :disabled="modelBuildMeta.isLoadExisted || isLoadingNewRange" value-format="timestamp" @change="(v) => handleChangeDateTime(v, 'end')" :is-auto-complete="true" :format="partitionFormat" > </el-date-picker> <common-tip :content="noPartition ? $t('partitionFirst'):$t('detectAvailableRange')" placement="top"> <el-button size="medium" class="auto-detect-btn ksd-ml-10" v-if="$store.state.project.projectPushdownConfig&&!isStreamModel" :disabled="modelBuildMeta.isLoadExisted || noPartition" :loading="isLoadingNewRange" icon="el-ksd-icon-data_range_search_old" @click="handleLoadNewestRange"> </el-button> </common-tip> <span style="position:absolute;width:1px; height:0" @click="handleLoadNewestRange"></span> <span style="position:absolute;width:1px; height:0" v-if="modelBuildMeta.dataRangeVal[0] && modelBuildMeta.dataRangeVal[1]"></span> </div> <p v-if="dateErrorMsg" class="error-date-range error-msg">{{dateErrorMsg}}</p> <div class="timestamp-format-tips" v-if="partitionMeta.format.indexOf('TIMESTAMP') !== -1 && (modelBuildMeta.dataRangeVal[0] || modelBuildMeta.dataRangeVal[1])"> <span>{{partitionMeta.format}}: </span> <span v-if="modelBuildMeta.dataRangeVal[0]">{{partitionMeta.format === 'TIMESTAMP MILLISECOND' ? new Date(modelBuildMeta.dataRangeVal[0]).getTime() : Math.round(new Date(modelBuildMeta.dataRangeVal[0]).getTime() / 1000)}}</span> - <span v-if="modelBuildMeta.dataRangeVal[1]">{{partitionMeta.format === 'TIMESTAMP MILLISECOND' ? new Date(modelBuildMeta.dataRangeVal[1]).getTime() : Math.round(new Date(modelBuildMeta.dataRangeVal[1]).getTime() / 1000)}}</span> </div> </el-form-item> </el-form> <el-alert v-show="!isShowRangeDateError && modelBuildMeta.dataRangeVal[1]" class="date-range-alert" :title="$t('segmentDateRangeTips')" type="warning" :closable="false" ></el-alert> <div class="error-msg" v-if="isShowRangeDateError">{{loadRangeDateError}}</div> <div v-if="isShowErrorSegments" class="error_segments"> <el-alert type="error" :show-background="false" :closable="false" show-icon> <span class="overlaps-tips">{{$t('overlapsTips')}}</span> <a href="javascript:;" @click="toggleDetail">{{$t('kylinLang.common.seeDetail')}} <i class="el-icon-arrow-down" v-show="!showDetail"></i> <i class="el-icon-arrow-up" v-show="showDetail"></i> </a> </el-alert> <table class="ksd-table small-size" v-if="showDetail"> <tr class="ksd-tr" v-for="(s, index) in errorSegments" :key="index"> <td>{{s.start | toServerGMTDate}}</td> <td>{{s.end | toServerGMTDate}}</td> </tr> </table> </div> <div v-if="displaySubPartition"> <div class="ksd-title-label-small ksd-mt-15">{{$t('multiPartitionValue')}}</div> <p class="sub-partition-alert"><i class="icon el-icon-ksd-alert ksd-mr-5"></i>{{$t('subPartitionAlert')}}</p> <arealabel ref="selectSubPartition" :class="['select-sub-partition', {'error-border': duplicateValueError}, 'ksd-mt-5']" :duplicateremove="false" splitChar="," :selectedlabels="selectMultiPartitionValues" :isNeedNotUpperCase="true" :allowcreate="true" :isSignSameValue="true" :remoteSearch="true" :labels="subPartitionOptions" :placeholder="$t('multiPartitionPlaceholder')" :remote-method="filterPartitions" :datamap="{label: 'label', value: 'value'}" :selectGroupOne="subPartitionGroupOne" @duplicateTags="checkDuplicateValue" @refreshData="refreshPartitionValues" @removeTag="removeSelectedMultiPartition"> </arealabel> <p class="duplicate-tips" v-if="duplicateValueError"><span class="error-msg">{{$t('duplicatePartitionValueTip')}}</span><span class="clear-value-btn" @click="removeDuplicateValue"><i class="el-icon-ksd-clear ksd-mr-5"></i>{{$t('removeDuplicateValue')}}</span></p> </div> </div> </div> <div slot="footer" class="dialog-footer ky-no-br-space"> <!-- <div class="ksd-fleft" v-if="$store.state.project.multi_partition_enabled&&modelDesc.multi_partition_desc&&modelDesc.multi_partition_desc.columns.length"> <el-checkbox v-model="isMultipleBuild"> <span>{{$t('multipleBuild')}}</span> <common-tip placement="top" :content="$t('multipleBuildTip')"> <span class='el-icon-ksd-what'></span> </common-tip> </el-checkbox> </div> --> <el-button @click="closeModal(false)" size="medium">{{$t('kylinLang.common.cancel')}}</el-button> <template v-if="isAddSegment"> <el-button type="primary" :loading="btnLoading&&!isBuildLoading&&!isWillAddIndex" @click="setbuildModel(false, 'onlySave')" :disabled="incrementalDisabled || disableFullLoad || btnLoading&&(isBuildLoading || isWillAddIndex)" size="medium">{{$t('kylinLang.common.save')}}</el-button> <el-button type="primary" :loading="btnLoading&&isBuildLoading&&!isWillAddIndex" v-if="modelDesc.total_indexes && !multiPartitionEnabled" @click="setbuildModel(true)" :disabled="incrementalDisabled || disableFullLoad || btnLoading&&!isBuildLoading" size="medium">{{$t('saveAndBuild')}}</el-button> <el-button type="primary" :loading="btnLoading&&!isBuildLoading&&isWillAddIndex" v-else-if="!multiPartitionEnabled" @click="saveAndAddIndex" :disabled="incrementalDisabled || disableFullLoad || btnLoading&&!isWillAddIndex" size="medium">{{$t('saveAndAddIndex')}}</el-button> </template> <template v-else> <el-button type="primary" :loading="btnLoading" @click="setbuildModel(true)" :disabled="incrementalDisabled || disableFullLoad || duplicateValueError" size="medium">{{$t(buildType)}}</el-button> </template> </div> </el-dialog> </template> <script> import Vue from 'vue' import { Component, Watch } from 'vue-property-decorator' import { mapState, mapMutations, mapActions, mapGetters } from 'vuex' import vuex from 'store' import { handleError, transToUTCMs, getGmtDateFromUtcLike, kylinMessage } from 'util/business' import { handleSuccessAsync, transToServerGmtTime, isDatePartitionType, isStreamingPartitionType, isSubPartitionType, kylinConfirm, split_array, objectClone, indexOfObjWithSomeKey } from 'util/index' import locales from './locales' import store, { types } from './store' import NModel from '../../ModelEdit/model.js' import { BuildIndexStatus } from 'config/model' import { dateFormats, timestampFormats, dateTimestampFormats } from 'config' import arealabel from '../../../../common/area_label.vue' import moment from 'moment' vuex.registerModule(['modals', 'ModelBuildModal'], store) @Component({ components: { arealabel }, computed: { ...mapGetters([ 'currentSelectedProject', 'datasourceActions' ]), ...mapState('ModelBuildModal', { isShow: state => state.isShow, title: state => state.title, source: state => state.source, type: state => state.type, isAddSegment: state => state.isAddSegment, buildOrComp: state => state.buildOrComp, isHaveSegment: state => state.isHaveSegment, disableFullLoad: state => state.disableFullLoad, modelDesc: state => state.form.modelDesc, modelInstance: state => state.form.modelInstance || state.form.modelDesc && new NModel(state.form.modelDesc) || null, callback: state => state.callback }), ...mapState({ multiPartitionEnabled: state => state.project.multi_partition_enabled }) }, methods: { ...mapActions({ buildModel: 'MODEL_BUILD', checkDataRange: 'CHECK_DATA_RANGE', buildFullLoadModel: 'MODEL_FULLLOAD_BUILD', buildIndex: 'BUILD_INDEX', fetchNewestModelRange: 'GET_MODEL_NEWEST_RANGE', fetchPartitionFormat: 'FETCH_PARTITION_FORMAT', updataModel: 'UPDATE_MODEL', autoFixSegmentHoles: 'AUTO_FIX_SEGMENT_HOLES', setModelPartition: 'MODEL_PARTITION_SET', fetchSubPartitionValues: 'FETCH_SUB_PARTITION_VALUES', validateDateFormat: 'VALIDATE_DATE_FORMAT' }), ...mapMutations('ModelBuildModal', { setModal: types.SET_MODAL, hideModal: types.HIDE_MODAL, setModalForm: types.SET_MODAL_FORM, resetModalForm: types.RESET_MODAL_FORM }), ...mapActions('DetailDialogModal', { callGlobalDetailDialog: 'CALL_MODAL' }) }, locales }) export default class ModelBuildModal extends Vue { moment = moment btnLoading = false isLoadingNewRange = false modelBuildMeta = { dataRangeVal: [], isLoadExisted: false } rules = {} loadRangeDateError = '' isShowRangeDateError = false isShowErrorSegments = false showDetail = false errorSegments = [] buildType = '' defaultBuildType = '' buildOrComplete = 'build' partitionMeta = { table: '', column: '', format: '', multiPartition: '' } prevPartitionMeta = { table: '', column: '', format: '', multiPartition: '' } partitionRules = { column: [{validator: this.validateBrokenColumn, trigger: 'change'}] } isLoadingFormat = false dateFormats = dateFormats timestampFormats = timestampFormats dateTimestampFormats = dateTimestampFormats isExpand = true isShowWarning = false isWillAddIndex = false inputMultiType = 'select' multiPartitionValues = [] multiPartitionValueOptions = [] selectMultiPartitionValues = [] modelSubPartitionValues = [] isMultipleBuild = false duplicateValueError = false subPartitionOptions = [] subPartitionGroupOne = [] dateErrorMsg = '' formatedDate = '' errorFormat = '' isExpandFormatRule = false timestamp = Date.now().toString(32) secondStoragePartitionTips = false isBuildLoading = false @Watch('buildType') changeBuildType (newVal, oldVal) { newVal !== oldVal && (this.duplicateValueError = false) } toggleInputMultiPartition () { this.inputMultiType = this.inputMultiType === 'select' ? 'textarea' : 'select' } get modelId () { // batch 和 streaming 的都是取 uuid if (this.modelDesc.model_type !== 'HYBRID') { return this.modelDesc.uuid } else { // HYBRID 模式的,传批数据id return this.modelDesc.batch_id } } get toggleText () { return this.inputMultiType === 'select' ? this.$t('selectInput') : this.$t('batchInput') } get displaySubPartition () { return this.source !== 'addSegment' && this.$store.state.project.multi_partition_enabled && this.partitionMeta.multiPartition } get isStreamModel () { const factTable = this.modelInstance.getFactTable() return factTable.source_type === 1 || this.modelDesc.model_type === 'STREAMING' } get isNotBatchModel () { const factTable = this.modelInstance.getFactTable() return factTable.source_type === 1 || this.modelInstance.model_type !== 'BATCH' } get isHybridModel () { return this.modelDesc.model_type === 'HYBRID' } get dateFormatsOptions () { return this.isNotBatchModel ? timestampFormats : dateFormats } refreshPartitionValues (val) { this.multiPartitionValues = val } removeSelectedMultiPartition () {} toggleDetail () { this.showDetail = !this.showDetail } get fullLoadBuildTips () { return this.$t('fullLoadBuildTips', {storageSize: Vue.filter('dataSize')(this.modelDesc.storage)}) } get noPartition () { return !(this.partitionMeta.table && this.partitionMeta.column && this.partitionMeta.format) } get incrementalDisabled () { return !(this.partitionMeta.table && this.partitionMeta.column && this.partitionMeta.format && this.modelBuildMeta.dataRangeVal.length && this.modelBuildMeta.dataRangeVal[0] && this.modelBuildMeta.dataRangeVal[1]) && this.buildType === 'incremental' } get partitionFormat () { if (this.partitionMeta.format === 'TIMESTAMP SECOND') { return 'yyyy-MM-dd HH:mm:ss' } if (this.partitionMeta.format === 'TIMESTAMP MILLISECOND') { return 'yyyy-MM-dd HH:mm:ss.SSS' } return this.partitionMeta.format } handChangeBuildType () { if (this.buildType === 'incremental' && !this.partitionMeta.table) { this.isExpand = true this.partitionMeta.table = this.partitionTables[0].alias } this.dateErrorMsg = '' this.secondStoragePartitionTips = false this.isShowWarning = this.isHaveSegment && (this.buildType !== this.defaultBuildType || JSON.stringify(this.prevPartitionMeta) !== JSON.stringify(this.partitionMeta)) } handleChangeDateTime (val, pos) { // 仅在 IE 浏览器下做兼容处理 if (val && navigator.userAgent.indexOf('Windows NT') >= 0) { const newDate = function (date) { if (typeof date === 'string' && /^\d{4}-\d{2}-\d{2}/.test(date)) { return new Date(date.replace(/-/g, '/')) } else { return new Date(date) } } let dateVal = this.modelBuildMeta.dataRangeVal pos === 'start' ? dateVal.splice(0, 1, newDate(val).getTime()) : dateVal.splice(1, 1, newDate(val).getTime()) this.$set(this.modelBuildMeta, 'dataRangeVal', dateVal) } this.resetError() } validateBrokenColumn (rule, value, callback) { if (value) { if (this.checkIsBroken(this.brokenPartitionColumns, value)) { return callback(new Error(this.$t('noColumnFund'))) } } if (!value && this.partitionMeta.table) { return callback(new Error(this.$t('pleaseInputColumn'))) } callback() } checkIsBroken (brokenKeys, key) { if (key) { return ~brokenKeys.indexOf(key) } return false } // 获取破损的partition keys get brokenPartitionColumns () { if (this.partitionMeta.table) { let ntable = this.modelInstance.getTableByAlias(this.partitionMeta.table) return this.modelInstance.getBrokenModelLinksKeys(ntable.guid, [this.partitionMeta.column]) } return [] } get selectedTable () { if (this.partitionMeta.table) { for (let i = 0; i < this.partitionTables.length; i++) { if (this.partitionTables[i].alias === this.partitionMeta.table) { return this.partitionTables[i] } } } } async handleLoadFormat () { try { this.isLoadingFormat = true const ccColumns = this.modelInstance.getComputedColumns() const index = indexOfObjWithSomeKey(ccColumns, 'columnName', this.partitionMeta.column) const data = { project: this.currentSelectedProject, table: this.selectedTable.name, partition_column: this.partitionMeta.column } if (index !== -1) { // 分区列是CC列 data.expression = ccColumns[index].innerExpression } const response = await this.fetchPartitionFormat(data) this.partitionMeta.format = await handleSuccessAsync(response) this.partitionColumnFormatChange(this.partitionMeta.format) this.isLoadingFormat = false } catch (e) { this.isLoadingFormat = false handleError(e) } } get columns () { if (!this.isShow || this.partitionMeta.table === '') { return [] } let result = [] let factTable = this.modelInstance.getFactTable() if (factTable) { factTable.columns.forEach((x) => { if (this.isNotBatchModel && isStreamingPartitionType(x.datatype)) { result.push(x) } else if (!this.isNotBatchModel && isDatePartitionType(x.datatype)) { result.push(x) } }) } let ccColumns = this.modelInstance.getComputedColumns() let cloneCCList = objectClone(ccColumns) cloneCCList.forEach((x) => { let cc = { name: x.columnName, datatype: x.datatype } result.push(cc) }) return result } get subPartitionColumnsOptions () { if (!this.isShow || this.partitionMeta.table === '') { return [] } let result = [] let factTable = this.modelInstance.getFactTable() if (factTable) { factTable.columns.forEach((x) => { if (isSubPartitionType(x.datatype)) { result.push(x) } }) } return result } // 分区设置改变 changePartitionSetting () { if (JSON.stringify(this.prevPartitionMeta) !== JSON.stringify(this.partitionMeta) || this.buildType !== this.defaultBuildType) { if (this.isHaveSegment) { this.isShowWarning = true } } else { this.isShowWarning = false } } partitionTableChange () { this.partitionMeta.column = '' this.partitionMeta.format = '' this.$refs.partitionForm.validate() this.modelBuildMeta.dataRangeVal = [] this.secondStoragePartitionTips = false this.changePartitionSetting() } partitionColumnChange () { // this.partitionMeta.format = 'yyyy-MM-dd' // this.$refs.partitionForm.validate() this.modelBuildMeta.dataRangeVal = [] this.secondStoragePartitionTips = false this.changePartitionSetting() } async partitionColumnFormatChange (val) { this.formatedDate = '' this.errorFormat = '' if (val) { try { const res = await this.validateDateFormat({partition_date_column: this.partitionMeta.column, partition_date_format: this.partitionMeta.format}) this.formatedDate = await handleSuccessAsync(res) } catch (e) { this.errorFormat = e.body.msg this.formatedDate = '' } } this.modelBuildMeta.dataRangeVal = [] this.changePartitionSetting() } get partitionTables () { let result = [] if (this.isShow && this.modelInstance) { Object.values(this.modelInstance.tables).forEach((nTable) => { if (nTable.kind === 'FACT') { result.push(nTable) } }) } return result } toggleShowPartition () { this.isExpand = !this.isExpand } showToolTip (value) { let len = 0 value.split('').forEach((v) => { if (/[\u4e00-\u9fa5]/.test(v)) { len += 2 } else { len += 1 } }) return len <= 15 } get buildTips () { if (this.buildType === 'incremental') { return this.$t('incrementalTips') } else if (this.buildType === 'fullLoad') { return this.$t('fullLoadTips', {storageSize: Vue.filter('dataSize')(this.modelDesc.storage)}) } } get format () { return this.modelDesc.partition_desc && this.modelDesc.partition_desc.partition_date_format || 'yyyy-MM-dd' } formatPartition () { let format = '' switch (this.partitionMeta.format) { case 'yyyy-MM-dd': case 'yyyyMMdd': case 'yyyy/MM/dd': format = 'YYYY/MM/DD' break case 'yyyy-MM': case 'yyyyMM': format = 'YYYY/MM' break case 'yyyy-MM-dd HH:mm:ss': format = 'YYYY/MM/DD HH:mm:ss' break case 'yyyy-MM-dd HH:mm:ss.SSS': format = 'YYYY/MM/DD HH:mm:ss.SSS' break } return format } validateRange (value) { return new Promise((resolve, reject) => { const [ startValue, endValue ] = value let format = this.formatPartition() const formatTimestampStart = !format ? startValue : (startValue && new Date(moment(new Date(startValue)).format(format)).getTime()) const formatTimestampEnd = !format ? endValue : (endValue && new Date(moment(new Date(endValue)).format(format)).getTime()) const isLoadExisted = this.modelBuildMeta.isLoadExisted if ((!startValue || !endValue || transToUTCMs(formatTimestampStart) > transToUTCMs(formatTimestampEnd)) && !isLoadExisted) { // callback(new Error(this.$t('invaildDate'))) this.dateErrorMsg = this.$t('invaildDate') reject() } else if (startValue && endValue && transToUTCMs(formatTimestampStart) === transToUTCMs(formatTimestampEnd) && !isLoadExisted) { // callback(new Error(this.$t('invaildDateNoEqual'))) this.dateErrorMsg = this.$t('invaildDateNoEqual') reject() } else { // callback() this.dateErrorMsg = '' resolve() } }) } @Watch('isShow') async initModelBuldRange () { if (this.isShow) { this.buildType = this.type this.defaultBuildType = this.type this.buildOrComplete = this.buildOrComp this.modelBuildMeta.dataRangeVal = [] if (this.modelDesc.last_build_end && this.buildType === 'incremental') { let lastBuildDate = getGmtDateFromUtcLike(+this.modelDesc.last_build_end) if (lastBuildDate) { this.modelBuildMeta.dataRangeVal.push(lastBuildDate, lastBuildDate) } } this.$nextTick(() => { this.$refs.partitionForm && this.$refs.partitionForm.validate() }) if (this.modelDesc && this.modelDesc.partition_desc && this.modelDesc.partition_desc.partition_date_column && this.buildType === 'incremental') { let named = this.modelDesc.partition_desc.partition_date_column.split('.') this.partitionMeta.table = this.prevPartitionMeta.table = named[0] this.partitionMeta.column = this.prevPartitionMeta.column = named[1] this.partitionMeta.format = this.prevPartitionMeta.format = this.modelDesc.partition_desc.partition_date_format this.partitionMeta.multiPartition = this.prevPartitionMeta.multiPartition = this.modelDesc.multi_partition_desc && this.modelDesc.multi_partition_desc.columns[0] && this.modelDesc.multi_partition_desc.columns[0].split('.')[1] || '' this.isExpand = false } else { this.isExpand = true } this.isShowWarning = false if (this.$store.state.project.multi_partition_enabled && this.modelDesc.multi_partition_desc && this.modelDesc.multi_partition_desc.columns.length) { try { const res = await this.fetchSubPartitionValues({ project: this.currentSelectedProject, model_id: this.modelId }) const data = await handleSuccessAsync(res) this.modelSubPartitionValues = data.map((p) => { return p.partition_value[0] }) this.modelSubPartitionValues.length && (this.subPartitionGroupOne = [{label: this.$t('selectAllSubPartitions'), value: `select_all_${this.timestamp}`}]) this.subPartitionOptions = this.modelSubPartitionValues.slice(0, 50).map(it => ({label: it, value: it})) } catch (e) { handleError(e) } } } else { this.modelBuildMeta.dataRangeVal = [] this.resetForm() } } resetForm () { this.partitionMeta = { table: '', column: '', format: '', multiPartition: '' } this.prevPartitionMeta = { table: '', column: '', format: '', multiPartition: '' } this.filterCondition = '' this.isLoadingSave = false this.isLoadingFormat = false this.secondStoragePartitionTips = false } async handleLoadNewestRange () { this.$refs.buildForm && this.$refs.buildForm.clearValidate() this.isLoadingNewRange = true this.resetError() const partition_desc = { partition_date_column: this.partitionMeta.table + '.' + this.partitionMeta.column, partition_date_format: this.partitionMeta.format } const ccColumns = this.modelInstance.getComputedColumns() const index = indexOfObjWithSomeKey(ccColumns, 'columnName', this.partitionMeta.column) try { const submitData = { project: this.currentSelectedProject, model: this.modelId, partition_desc: partition_desc } if (index !== -1) { // 分区列是CC列 submitData.expression = ccColumns[index].innerExpression } const response = await this.fetchNewestModelRange(submitData) if (submitData.model !== this.modelId) { // 避免ajax耗时太长导致会覆盖新的model的load range数据 return } if (response.body.code === '000') { const result = await handleSuccessAsync(response) const startTime = +result.start_time const endTime = +result.end_time this.modelBuildMeta.dataRangeVal = [ getGmtDateFromUtcLike(startTime), getGmtDateFromUtcLike(endTime) ] } else if (response.body.code === '999') { this.loadRangeDateError = response.body.msg this.isShowRangeDateError = true } } catch (e) { handleError(e) } this.isLoadingNewRange = false } resetError () { this.loadRangeDateError = '' this.dateErrorMsg = '' this.isShowRangeDateError = false this.isShowErrorSegments = false this.errorSegments = [] this.showDetail = false } closeModal (isSubmit) { this.isLoadingNewRange = false this.btnLoading = false this.$refs.buildForm && this.$refs.buildForm.resetFields() this.isShowErrorSegments = false this.errorSegments = [] this.showDetail = false this.isWillAddIndex = false this.multiPartitionValues = [] this.subPartitionGroupOne = [] this.dateErrorMsg = '' this.isExpandFormatRule = false this.resetError() this.hideModal() setTimeout(() => { this.callback && this.callback(isSubmit) this.resetModalForm() }, 200) } _buildModel ({start, end, modelId, modelName, isBuild, partition_desc, multi_partition_desc, multi_partition_values, build_all_sub_partitions, segment_holes}) { const partitionValuesArr = split_array(multi_partition_values, 1) this.buildModel({ model_id: modelId, data: { start: start, end: end, build_all_indexes: isBuild, partition_desc: partition_desc, sub_partition_values: partitionValuesArr, segment_holes: segment_holes || [], // parallel_build_by_segment: this.isMultipleBuild, multi_partition_desc, build_all_sub_partitions, project: this.currentSelectedProject } }).then(() => { this.btnLoading = false // this.$emit('refreshModelList') if (this.isWillAddIndex) { this.$emit('isWillAddIndex', modelName) } else { if (isBuild) { this.$message({ dangerouslyUseHTMLString: true, type: 'success', customClass: 'build-full-load-success', duration: 10000, showClose: true, message: ( <div class="el-message__content"> <span>{this.$t('kylinLang.common.buildSuccess')}</span> <a href="javascript:void(0)" onClick={() => this.jumpToJobs()}>{this.$t('kylinLang.common.toJoblist')}</a> </div> ) }) } else { kylinMessage(this.$t('kylinLang.common.submitSuccess')) } } this.closeModal(true) }, (res) => { this.btnLoading = false res && handleError(res) }) } saveAndAddIndex () { this.isWillAddIndex = true this.setbuildModel(false) } async setbuildModel (isBuild, type) { this.isBuildLoading = isBuild this.btnLoading = true try { if (this.buildType === 'incremental' && this.buildOrComplete === 'build') { await this.validateRange(this.modelBuildMeta.dataRangeVal) this.$refs.buildForm.validate(async (valid) => { if (!valid) { this.btnLoading = false return } await (this.$refs.rangeForm && this.$refs.rangeForm.validate()) || Promise.resolve() await (this.$refs.partitionForm && this.$refs.partitionForm.validate()) || Promise.resolve() const { column, table } = this.partitionMeta if (this.modelDesc.second_storage_enabled && !(this.modelDesc.partition_column_in_dims || this.modelDesc.simplified_dimensions.map(it => it.column).includes(`${table}.${column}`))) { this.secondStoragePartitionTips = true this.btnLoading = false return } if (type && type === 'onlySave') { try { await this.$msgbox({ title: this.$t('kylinLang.common.tip'), message: <div><p>{this.$t('onlySaveTip1')}</p><p>{this.$t('onlySaveTip2')}</p></div>, showCancelButton: true, confirmButtonText: this.$t('kylinLang.common.save') }) } catch (e) { this.btnLoading = false return } } const partition_desc = {} if (typeof this.modelDesc.available_indexes_count === 'number' && this.modelDesc.available_indexes_count > 0) { if (this.prevPartitionMeta.table !== this.partitionMeta.table || this.prevPartitionMeta.column !== this.partitionMeta.column || this.prevPartitionMeta.format !== this.partitionMeta.format || this.prevPartitionMeta.multiPartition !== this.partitionMeta.multiPartition) { try { await kylinConfirm(this.$t('changeSegmentTips'), {confirmButtonText: this.$t('kylinLang.common.save'), type: 'warning', dangerouslyUseHTMLString: true}, this.$t('kylinLang.common.tip')) } catch (e) { this.btnLoading = false return false } } } partition_desc.partition_date_column = this.partitionMeta.table + '.' + this.partitionMeta.column partition_desc.partition_date_format = this.partitionMeta.format let start = null let end = null if (!this.modelBuildMeta.isLoadExisted) { start = transToUTCMs(this.modelBuildMeta.dataRangeVal[0]) end = transToUTCMs(this.modelBuildMeta.dataRangeVal[1]) } // 如果切换分区列或者构建方式,会清空segment,不用检测 const isChangePatition = this.prevPartitionMeta.table && (this.prevPartitionMeta.table !== this.partitionMeta.table || this.prevPartitionMeta.column !== this.partitionMeta.column || this.prevPartitionMeta.format !== this.partitionMeta.format) const isChangeBuildType = !this.prevPartitionMeta.table && this.isHaveSegment const multi_partition_desc = this.partitionMeta.multiPartition ? {columns: [this.partitionMeta.table + '.' + this.partitionMeta.multiPartition]} : null let build_all_sub_partitions = false const partitionValues = JSON.parse(JSON.stringify(this.multiPartitionValues)) if (this.multiPartitionValues.includes(`select_all_${this.timestamp}`)) { const index = this.multiPartitionValues.indexOf(`select_all_${this.timestamp}`) build_all_sub_partitions = true index >= 0 && partitionValues.splice(index, 1) } if (isChangePatition || isChangeBuildType) { this._buildModel({start: start, end: end, modelId: this.modelId, modelName: this.modelDesc.alias, isBuild: isBuild, partition_desc: partition_desc, multi_partition_desc, multi_partition_values: partitionValues, build_all_sub_partitions}) } else { let res try { res = await this.checkDataRange({modelId: this.modelId, project: this.currentSelectedProject, start: start, end: end}) } catch (e) { this.btnLoading = false handleError(e) } const data = await handleSuccessAsync(res) if (data) { if (data.overlap_segments.length) { this.btnLoading = false this.isShowErrorSegments = true this.errorSegments = data.overlap_segments } else if (data.segment_holes.length) { const tableData = [] let selectSegmentHoles = [] const segmentHoles = data.segment_holes segmentHoles.forEach((seg) => { const obj = {} obj['start'] = transToServerGmtTime(seg.start) obj['end'] = transToServerGmtTime(seg.end) obj['date_range_start'] = seg.start obj['date_range_end'] = seg.end tableData.push(obj) }) try { await this.callGlobalDetailDialog({ msg: this.$t('segmentHoletips', {modelName: this.modelDesc.name}), title: this.$t('fixSegmentTitle'), detailTableData: tableData, detailColumns: [ {column: 'start', label: this.$t('kylinLang.common.startTime')}, {column: 'end', label: this.$t('kylinLang.common.endTime')} ], isShowSelection: true, dialogType: 'warning', showDetailBtn: false, needResolveCancel: true, cancelText: this.$t('ignore'), submitText: this.$t('fixAndBuild'), onlyCloseDialogReject: true, customCallback: async (segments) => { selectSegmentHoles = segments.map((seg) => { return {start: seg.date_range_start, end: seg.date_range_end} }) try { this._buildModel({start: start, end: end, modelId: this.modelId, modelName: this.modelDesc.alias, isBuild: isBuild, partition_desc: partition_desc, multi_partition_desc, multi_partition_values: partitionValues, build_all_sub_partitions, segment_holes: selectSegmentHoles}) } catch (e) { handleError(e) } } }) this._buildModel({start: start, end: end, modelId: this.modelId, modelName: this.modelDesc.alias, isBuild: isBuild, partition_desc: partition_desc, multi_partition_desc, multi_partition_values: partitionValues, build_all_sub_partitions}) } catch (e) { this.btnLoading = false handleError(e) } } else { this._buildModel({start: start, end: end, modelId: this.modelId, modelName: this.modelDesc.alias, isBuild: isBuild, partition_desc: partition_desc, multi_partition_desc, multi_partition_values: partitionValues, build_all_sub_partitions}) } } } }) } else if (this.buildType === 'fullLoad' && this.buildOrComplete === 'build') { if (this.modelDesc && this.modelDesc.partition_desc && this.modelDesc.partition_desc.partition_date_column) { try { await kylinConfirm(this.$t('changeSegmentTips'), {confirmButtonText: this.$t('kylinLang.common.save'), type: 'warning', dangerouslyUseHTMLString: true}, this.$t('kylinLang.common.tip')) } catch (e) { this.btnLoading = false return false } this.btnLoading = true await this.setModelPartition({modelId: this.modelId, project: this.currentSelectedProject, partition_desc: null}) } this.buildFullLoadModel({ model_id: this.modelId, start: null, end: null, build_all_indexes: isBuild, project: this.currentSelectedProject }).then(async () => { this.btnLoading = false if (this.isWillAddIndex) { this.$emit('isWillAddIndex') } else { if (isBuild) { this.$message({ dangerouslyUseHTMLString: true, type: 'success', customClass: 'build-full-load-success', duration: 10000, showClose: true, message: ( <div class="el-message__content"> <span>{this.$t('kylinLang.common.buildSuccess')}</span> <a href="javascript:void(0)" onClick={() => this.jumpToJobs()}>{this.$t('kylinLang.common.toJoblist')}</a> </div> ) }) } else { kylinMessage(this.$t('kylinLang.common.submitSuccess')) } } this.closeModal(true) }, (res) => { this.btnLoading = false res && handleError(res) }) } } catch (e) { this.btnLoading = false handleError(e) } } // 补全索引已拿掉 async completeBuildModel () { if (this.modelDesc.segment_holes.length) { const segmentHoles = this.modelDesc.segment_holes try { const tableData = [] let selectSegmentHoles = [] segmentHoles.forEach((seg) => { const obj = {} obj['start'] = transToServerGmtTime(seg.date_range_start) obj['end'] = transToServerGmtTime(seg.date_range_end) obj['date_range_start'] = seg.date_range_start obj['date_range_end'] = seg.date_range_end tableData.push(obj) }) await this.callGlobalDetailDialog({ msg: this.$t('segmentHoletips', {modelName: this.modelDesc.name}), title: this.$t('fixSegmentTitle'), detailTableData: tableData, detailColumns: [ {column: 'start', label: this.$t('kylinLang.common.startTime')}, {column: 'end', label: this.$t('kylinLang.common.endTime')} ], isShowSelection: true, dialogType: 'warning', showDetailBtn: false, needResolveCancel: true, cancelText: this.$t('ignore'), submitText: this.$t('fixAndBuild'), customCallback: async (segments) => { selectSegmentHoles = segments.map((seg) => { return {start: seg.date_range_start, end: seg.date_range_end} }) try { await this.autoFixSegmentHoles({project: this.currentSelectedProject, model_id: this.modelId, segment_holes: selectSegmentHoles}) } catch (e) { handleError(e) } this.confirmBuild() } }) this.confirmBuild() } catch (e) { e !== 'cancel' && handleError(e) } } else { this.confirmBuild() } } async confirmBuild () { try { this.btnLoading = true let res = await this.buildIndex({ project: this.currentSelectedProject, model_id: this.modelId }) let data = await handleSuccessAsync(res) this.handleBuildIndexTip(data) this.closeModal(true) } catch (e) { handleError(e) } finally { this.btnLoading = false } } handleBuildIndexTip (data) { let tipMsg = '' if (data.type === BuildIndexStatus.NORM_BUILD) { tipMsg = this.$t('kylinLang.model.buildIndexSuccess') this.$message({message: tipMsg, type: 'success'}) return } if (data.type === BuildIndexStatus.NO_LAYOUT) { tipMsg = this.$t('kylinLang.model.buildIndexFail2', {indexType: this.$t('kylinLang.model.index')}) } else if (data.type === BuildIndexStatus.NO_SEGMENT) { tipMsg += this.$t('kylinLang.model.buildIndexFail1', {modelName: this.modelDesc.name}) } this.$confirm(tipMsg, this.$t('kylinLang.common.notice'), {showCancelButton: false, type: 'warning', dangerouslyUseHTMLString: true}) } // 检测是否输入了重复的子分区值 checkDuplicateValue (type) { this.duplicateValueError = type } removeDuplicateValue () { this.$refs.selectSubPartition && this.$refs.selectSubPartition.clearDuplicateValue() } filterPartitions (query) { if (query) { this.subPartitionGroupOne = [] } else if (this.modelSubPartitionValues.length) { this.subPartitionGroupOne = [{label: this.$t('selectAllSubPartitions'), value: `select_all_${this.timestamp}`}] } this.subPartitionOptions = this.modelSubPartitionValues.filter(item => item.indexOf(query) >= 0).slice(0, 50).map(it => ({label: it, value: it})) } jumpToJobs () { this.$router.push('/monitor/job') } created () { this.$on('buildModel', this._buildModel) } } </script> <style lang="less"> @import '../../../../../assets/styles/variables.less'; .model-build { .error-format { color: @error-color-1; font-size: 12px; line-height: 16px; } .timestamp-format-tips, .pre-format { color: @text-normal-color; font-size: 14px; margin-top: 4px; background-color: @base-background-color; height: 26px; line-height: 26px; border-radius: 4px; display: inline-block; padding: 0 4px; } .format { font-size: 12px; line-height: 16px; color: @text-disabled-color; margin-top: 4px; span { color: @base-color; cursor: pointer; .arrow { transform: rotate( 90deg ); margin-left: 3px; } } } .date-range-alert { padding: 10px 0; background-color: transparent; } .auto-detect-btn { line-height: 22px; } .detail-content { background-color: @base-background-color-1; padding: 8px 16px; box-sizing: border-box; font-size: 12px; color: @text-normal-color; line-height: 16px; margin-top: 5px; p { display: flex; } } .habird-tips { font-size: 14px; } .tips { font-size: 12px; color: @text-disabled-color; } .add-title { font-weight: bold; color: @text-title-color; } .item-desc { font-size: 12px; line-height: 1; } .el-date-editor.is-error { .el-input__inner { border: 1px solid @error-color-1; } } .error-date-range { padding: 0; margin: 0; line-height: 1; } .error-msg { color: @error-color-1; font-size: 12px; margin-top: 5px; } .error_segments { .el-alert__icon { padding-left: 2px; } .overlaps-tips { color: @error-color-1; } } .error_segments a:hover { text-decoration: none; } .divide-block { color: @base-color; font-size: 12px; text-align: center; cursor: pointer; } .divide-line { border-top: 1px solid @line-border-color; margin-top: 10px; margin-bottom: 20px; } .area-input { border: 1px solid @line-border-color3; height: 60px; overflow-y: auto; margin-top: 5px; .el-input__inner { border: none; padding: 0 26px 0 12px; } .el-select .el-input__suffix { display: none; } } .duplicate-tips { font-size: 12px; margin-top: 5px; .clear-value-btn { cursor: pointer; color: @text-normal-color; &:hover { color: @base-color; } } } .sub-partition-alert { font-size: 12px; color: @text-title-color; margin: 10px 0; .icon { color: @text-disabled-color; } } .select-sub-partition.error-border { .el-input__inner { border-color: @error-color-1; } } .select-sub-partition { // 针对 IE 游览器下中文输入法下输入文字后面会自带叉叉问题 .el-select__input::-ms-clear { display: none; } } .el-icon-info { font-size: 14px; padding-left: 1px; } .segment-tips { line-height: 18px; margin-bottom: 10px; } .alert-icon { color: @text-disabled-color; } } .build-full-load-success { padding: 10px 30px 10px 10px; align-items: center; } </style>