This is an automated email from the ASF dual-hosted git repository. liyang pushed a commit to branch kylin5 in repository https://gitbox.apache.org/repos/asf/kylin.git
commit 671942f2460d40c8872e22b1df2c988c392ccda5 Author: ShengHuang <[email protected]> AuthorDate: Mon Sep 2 10:53:49 2024 +0800 KYLIN-5958 Support storage v3 for website --- .../common/CustomTransferData/TransferData.vue | 93 +++++----- .../src/components/monitor/batchJobs/handler.js | 10 +- kystudio/src/components/monitor/batchJobs/jobs.vue | 12 +- .../src/components/monitor/batchJobs/locales.js | 10 +- .../src/components/query/query_history_table.vue | 14 +- .../StudioModel/IndexOptimizationModal/index.vue | 199 +++++++++++++++++++++ .../StudioModel/IndexOptimizationModal/locales.js | 28 +++ .../StudioModel/IndexOptimizationModal/store.js | 44 +++++ .../StudioModel/ModelList/AggregateModal/index.vue | 6 + .../ModelList/IndexDetailModal/index.vue | 122 +++++++++++++ .../ModelList/IndexDetailModal/locales.js | 34 ++++ .../ModelList/IndexDetailModal/store.js | 44 +++++ .../StudioModel/ModelList/ModelActions/locales.js | 2 + .../ModelList/ModelActions/modelActions.vue | 38 +++- .../StudioModel/ModelList/ModelAggregate/index.vue | 55 +++++- .../ModelList/ModelAggregate/indexDetails.vue | 10 ++ .../ModelList/ModelAggregate/locales.js | 3 +- .../ModelList/ModelAggregateView/index.vue | 7 +- .../StreamingSegment/StreamingSegment.vue | 10 +- .../StudioModel/ModelList/ModelSegment/index.vue | 27 ++- .../StudioModel/ModelList/TableIndexView/index.vue | 6 + .../StudioModel/SettingStorageModal/index.vue | 130 ++++++++++++++ .../StudioModel/SettingStorageModal/locales.js | 20 +++ .../StudioModel/SettingStorageModal/store.js | 54 ++++++ .../StudioModel/TableIndexEdit/tableindex_edit.vue | 16 +- kystudio/src/config/spec.js | 10 +- kystudio/src/service/model.js | 27 +++ kystudio/src/store/model.js | 12 ++ kystudio/src/store/types.js | 4 + 29 files changed, 972 insertions(+), 75 deletions(-) diff --git a/kystudio/src/components/common/CustomTransferData/TransferData.vue b/kystudio/src/components/common/CustomTransferData/TransferData.vue index 2305331ba8..3546e2b709 100644 --- a/kystudio/src/components/common/CustomTransferData/TransferData.vue +++ b/kystudio/src/components/common/CustomTransferData/TransferData.vue @@ -44,13 +44,20 @@ </el-tooltip> </template> </template> - + </div> <div class="result-content" :class="{'is-no-footer': !hasLeftFooter}"> <div v-for="item in pagedModelColumns" :key="item.id" class="result-content_item"> <div class="checkbox-list"> <el-checkbox v-model="item.selected" :disabled="item.disabled" @change="handleIndexColumnChange(item)"> <el-tooltip :content="$t('excludedTableIconTip')" effect="dark" placement="top"><i class="excluded_table-icon el-icon-ksd-exclude" v-if="item.excluded"></i></el-tooltip> + <el-button + type="primary" + size="mini" + class="shardby is-shardby" + v-if="it.isShared&&showShardBy" + text + icon="el-ksd-n-icon-symbol-s-circle-filled"></el-button> <span :style="{'max-width': dragData.width - 140 + 'px', width: 'auto'}" :title="item.label" class="ksd-nobr-text">{{item.label}}</span> </el-checkbox> </div> @@ -156,9 +163,9 @@ </div> <i class="el-ksd-n-icon-info-circle-outlined ksd-ml-5"></i> </el-tooltip> - <el-tooltip placement="top" v-if="!it.isShared&sharedByAble" :content="shardByIndex !== -1 ? $t('replaceShardBy') : $t('setShardBy')"> + <el-tooltip placement="top" v-if="!it.isShared&sharedByAble&&showShardBy" :content="shardByIndex !== -1 ? $t('replaceShardBy') : $t('setShardBy')"> <el-button type="primary" size="mini" class="shardby" @click.stop="setShardBy(it.key)" text icon="el-ksd-n-icon-symbol-s-circle-filled"></el-button> - </el-tooltip><el-tooltip placement="top" v-if="it.isShared&sharedByAble" :content="$t('removeShardBy')"> + </el-tooltip><el-tooltip placement="top" v-if="showShardBy&&it.isShared&&sharedByAble" :content="$t('removeShardBy')"> <el-button type="primary" size="mini" class="shardby is-shardby" text @click.stop="unSetSharyBy(it.key)" icon="el-ksd-n-icon-symbol-s-circle-filled"></el-button> </el-tooltip><el-tooltip placement="top" :content="$t('remove')"> <i class="el-ksd-n-icon-close-outlined close-btn" @click.stop="removeColumn(it)"></i> @@ -185,11 +192,15 @@ import { objectClone, indexOfObjWithSomeKey, kylinConfirm } from 'util' import Sortable, { AutoScroll } from 'sortablejs/modular/sortable.core.esm.js' import EmptyData from '../EmptyData/EmptyData' - + Sortable.mount(new AutoScroll()) - + @Component({ props: { + showShardBy: { + type: Boolean, + default: true + }, allModelColumns: { type: Array, default () { @@ -341,17 +352,17 @@ } } hasDragSuccess = false - + get leftStyle () { return { width: this.dragData.width + 'px' } } - + get hasLeftFooter () { return !!this.$slots['left-footer'] && this.$slots['left-footer'].length } - + get alertItems () { let items = [] if (this.alertTips && this.alertTips.length > 0) { @@ -367,11 +378,11 @@ } return items } - + get columnTypes () { return [...new Set(this.allModelColumns.filter(it => it.type).map(it => it.type))] } - + get pagedModelColumns () { return this.filterModelColumns.slice(0, (this.pagination.left.pageOffset + 1) * this.pagination.left.pageSize) } @@ -379,15 +390,15 @@ get filterTypeOptionSelectedColumns () { return this.typeOptions.length ? this.filterSelectedColumns.filter(it => this.typeOptions.includes(it.type)) : this.filterSelectedColumns } - + getIndex (key) { return indexOfObjWithSomeKey(this.selectedData, 'key', key) } - + get pagedSelectedColumns () { return this.filterSelectedColumns.slice(0, (this.pagination.right.pageOffset + 1) * this.pagination.right.pageSize) } - + get filterSelectedColumns () { let selectedColumns = objectClone(this.selectedData) if (this.searchVar) { @@ -396,7 +407,7 @@ } return selectedColumns } - + get filterModelColumns () { // let allModelColumns = this.allModelColumns.filter(it => !this.selectedColumns.includes(it.key)).map(item => ({...item, selected: false})) let allModelColumns = this.allModelColumns.map(item => ({...item, selected: this.selectedColumns.includes(item.key)})) @@ -413,7 +424,7 @@ } } else { this.$set(this.pagination.left, 'pageOffset', 0) - + // const selectedColumns = allModelColumns.filter(it => this.selectedColumns.includes(it.key)) const results = [...allModelColumns.filter(it => this.typeOptions.includes(it.type))] if (!this.columnSort) { @@ -425,20 +436,20 @@ this.$set(this.pagination.left, 'totalSize', resultDatas.length) return resultDatas } - + get isAnyCardinalityCol () { return this.selectedData.length && this.selectedData.filter((c) => !!c.cardinality).length > 0 } - + get isPartCardinalityCol () { const cardinalityNum = this.selectedData.filter((c) => !!c.cardinality).length return this.selectedData.length && cardinalityNum > 0 && cardinalityNum < this.selectedData.length } - + get unSelectedData () { return this.allModelColumns } - + get selectedData () { let sData = [] if (this.selectedColumns.length) { @@ -461,16 +472,16 @@ this.$set(this.pagination.right, 'totalSize', sData.length) return sData } - + get shardByIndex () { return indexOfObjWithSomeKey(this.selectedData, 'isShared', true) } - + created () { this.pagination.left.totalSize = this.filterModelColumns.length this.pagination.right.totalSize = this.selectedData.length } - + mounted () { const $unSelectPage = this.$el.querySelector('.result-content') const $selectedPage = this.$el.querySelector('.selected-results_layout') @@ -528,7 +539,7 @@ } } } - + debounce (fn, dom, scrollType, delay) { let timer = null return function () { @@ -540,14 +551,14 @@ }, delay) } } - + destroyed () { const $unSelectPage = this.$el.querySelector('.result-content') const $selectedPage = this.$el.querySelector('.selected-results_layout') $unSelectPage && $unSelectPage.removeEventListener('scroll', this.handleScroll) $selectedPage && $selectedPage.removeEventListener('scroll', this.handleScroll) } - + handleScroll (dom, scrollType) { const scrollBoxDom = this.$el.querySelector(dom) const scrollT = scrollBoxDom.scrollTop @@ -557,11 +568,11 @@ this.scrollLoad(scrollType) } } - + handleTableIndexRecognize () { this.$emit('handleTableIndexRecognize') } - + selectAll () { let selectedList = objectClone(this.selectedColumns) // 除去disabled的列, @@ -579,7 +590,7 @@ this.handleEmitEvent('setSelectedColumns', selectedList) this.setSelectedColumns() } - + showLoadMore (type) { const { left, right } = this.pagination if (type === 'left') { @@ -588,33 +599,33 @@ return (right.pageOffset + 1) * right.pageSize < right.totalSize } } - + scrollLoad (type) { if (this.showLoadMore(type)) { this.addPagination(type) } } - + addPagination (type) { const page = this.pagination[type].pageOffset + 1 this.$set(this.pagination[type], 'pageOffset', page) } - + handleEmitEvent (name, value) { this.$emit(name, value) } - + handleSortCommand (sort) { this.columnSort = sort } - + setSelectedColumns () { this.$nextTick(() => { const selectedList = this.selectedData.map(d => d.key) this.handleEmitEvent('setSelectedColumns', selectedList) }) } - + async handleSortSelectedCommand (sort) { if (this.hasDragSuccess) { await kylinConfirm(' ', {confirmButtonText: this.$t('remove'), type: 'warning'}, this.$t('cofirmRemoveSort')) @@ -624,7 +635,7 @@ this.setSelectedColumns() this.hasDragSuccess = false } - + async handleSortCardinality (sort) { if (this.hasDragSuccess) { await kylinConfirm(' ', {confirmButtonText: this.$t('remove'), type: 'warning'}, this.$t('cofirmRemoveSort')) @@ -634,11 +645,11 @@ this.setSelectedColumns() this.hasDragSuccess = false } - + goToDataSource () { this.$router.push('/studio/source') } - + handleIndexColumnChange (v) { const selectedList = objectClone(this.selectedColumns) if (v.selected) { @@ -650,14 +661,14 @@ this.handleEmitEvent('setSelectedColumns', selectedList) this.setSelectedColumns() } - + removeColumn (v) { const selectedList = objectClone(this.selectedColumns) const index = selectedList.indexOf(v.key) index > -1 && selectedList.splice(index, 1) this.handleEmitEvent('setSelectedColumns', selectedList) } - + async topColumn (e, key) { e.stopPropagation() e.preventDefault() @@ -680,7 +691,7 @@ this.hasDragSuccess = true this.$message({ type: 'success', message: this.$t('topColSuccess') }) } - + setShardBy (key) { let message = '' if (this.shardByIndex !== -1) { @@ -692,7 +703,7 @@ this.handleEmitEvent('setShardbyCol', this.selectedData[index].label) this.$message({ type: 'success', message: message || this.$t('setShardBySuccess') }) } - + unSetSharyBy (key) { const index = indexOfObjWithSomeKey(this.selectedData, 'key', key) this.selectedData[index].isShared = false diff --git a/kystudio/src/components/monitor/batchJobs/handler.js b/kystudio/src/components/monitor/batchJobs/handler.js index a1fe4fc0bf..fdd02e8e54 100644 --- a/kystudio/src/components/monitor/batchJobs/handler.js +++ b/kystudio/src/components/monitor/batchJobs/handler.js @@ -9,7 +9,9 @@ export function getStepLineName (that, name) { 'Update Metadata': that.$t('updateMetadata'), 'Table Sampling': that.$t('tableSampling'), 'Build Snapshot': that.$t('buildSnapshot'), - 'Clean Up Intermediate Table': that.$t('clearUpIntermediateTable') + 'Clean Up Intermediate Table': that.$t('clearUpIntermediateTable'), + 'Optimize layout data': that.$t('LAYOUT_DATA_OPTIMIZE'), + LAYOUT_DATA_OPTIMIZE: that.$t('LAYOUT_DATA_OPTIMIZE') } return stepMap[name] } @@ -32,7 +34,11 @@ export function getSubTasksName (that, name) { 'Sample Table Data': that.$t('sampleTableData'), 'Build Snapshot': that.$t('buildSnapshot'), 'Build indexes by layer': that.$t('buildIndexesByLayer'), - 'Update flat table statistics': that.$t('updateFlatTableStatistics') + 'Update flat table statistics': that.$t('updateFlatTableStatistics'), + 'delete useless layout data': that.$t('deleteUselessLayoutData'), + 'Optimize layout data by repartition': that.$t('optimizeLayoutDataByRepartition'), + 'Optimize layout data by zorder': that.$t('optimizeLayoutDataByZorder'), + 'Optimize layout data by compaction': that.$t('optimizeLayoutDataByCompaction') } return subTaskNameMap[name] } diff --git a/kystudio/src/components/monitor/batchJobs/jobs.vue b/kystudio/src/components/monitor/batchJobs/jobs.vue index 50d2594b33..83f139c583 100644 --- a/kystudio/src/components/monitor/batchJobs/jobs.vue +++ b/kystudio/src/components/monitor/batchJobs/jobs.vue @@ -95,13 +95,15 @@ min-width="180" show-overflow-tooltip> <template slot-scope="scope"> - <template v-if="scope.row.job_name !== 'SNAPSHOT_REFRESH' && scope.row.job_name !== 'SNAPSHOT_BUILD'"> - <span v-if="scope.row.data_range_end==9223372036854776000">{{$t('fullLoad')}}</span> + <template v-if="scope.row.job_name !== 'SNAPSHOT_REFRESH' && scope.row.job_name !== 'SNAPSHOT_BUILD' && scope.row.job_name !== 'LAYOUT_DATA_OPTIMIZE'"> + <span v-if="scope.row.data_range_end==9223372036854776000 && ![...delSecJobTypes, ...otherJobTypes].includes(scope.row.job_name)">{{$t('fullLoad')}}</span> + <span v-else-if="scope.row.data_range_end==9223372036854776000 && [...delSecJobTypes, ...otherJobTypes].includes(scope.row.job_name)">{{$t('full')}}</span> <span v-else>{{scope.row.data_range_start | toServerGMTDate}} - {{scope.row.data_range_end | toServerGMTDate}}</span> </template> <template v-else> <span v-if="scope.row.snapshot_data_range === 'FULL'">{{$t('fullLoad')}}</span> <span v-else-if="scope.row.snapshot_data_range === 'INC'">{{$t('increamLoad')}}</span> + <span v-else-if="scope.row.job_name === 'LAYOUT_DATA_OPTIMIZE'">{{$t('fullOptimization')}}</span> <span v-else>{{scope.row.snapshot_data_range ? JSON.parse(scope.row.snapshot_data_range).splice(0, 10).join(', ') : ''}}</span> </template> </template> @@ -441,8 +443,10 @@ export default class JobsList extends Vue { jobsList = [] jobTotal = 0 allStatus = ['PENDING', 'RUNNING', 'FINISHED', 'ERROR', 'DISCARDED', 'STOPPED'] - jobTypeFilteArr = ['INDEX_REFRESH', 'INDEX_MERGE', 'INDEX_BUILD', 'INC_BUILD', 'TABLE_SAMPLING', 'SNAPSHOT_BUILD', 'SNAPSHOT_REFRESH', 'SUB_PARTITION_BUILD', 'SUB_PARTITION_REFRESH'] - tableJobTypes = ['TABLE_SAMPLING', 'SNAPSHOT_BUILD', 'SNAPSHOT_REFRESH'] + jobTypeFilteArr = ['INDEX_REFRESH', 'INDEX_MERGE', 'INDEX_BUILD', 'INC_BUILD', 'TABLE_SAMPLING', 'SNAPSHOT_BUILD', 'SNAPSHOT_REFRESH', 'SUB_PARTITION_BUILD', 'SUB_PARTITION_REFRESH', 'EXPORT_TO_SECOND_STORAGE', 'SECOND_STORAGE_NODE_CLEAN', 'SECOND_STORAGE_MODEL_CLEAN', 'SECOND_STORAGE_SEGMENT_CLEAN', 'SECOND_STORAGE_INDEX_CLEAN', 'SECOND_STORAGE_REFRESH_SECONDARY_INDEXES', 'LAYOUT_DATA_OPTIMIZE'] + tableJobTypes = ['TABLE_SAMPLING', 'SNAPSHOT_BUILD', 'SNAPSHOT_REFRESH', 'SECOND_STORAGE_NODE_CLEAN'] + delSecJobTypes = ['SECOND_STORAGE_NODE_CLEAN', 'SECOND_STORAGE_MODEL_CLEAN', 'SECOND_STORAGE_SEGMENT_CLEAN', 'SECOND_STORAGE_INDEX_CLEAN'] + otherJobTypes = ['SECOND_STORAGE_REFRESH_SECONDARY_INDEXES'] targetId = '' searchLoading = false batchBtnsEnabled = { diff --git a/kystudio/src/components/monitor/batchJobs/locales.js b/kystudio/src/components/monitor/batchJobs/locales.js index 1e6c803b8a..2d8fef1a79 100644 --- a/kystudio/src/components/monitor/batchJobs/locales.js +++ b/kystudio/src/components/monitor/batchJobs/locales.js @@ -76,7 +76,7 @@ export default { SUB_PARTITION_REFRESH: 'Refresh Sub-partitions Data', SUB_PARTITION_BUILD: 'Build Sub-partitions Data', SNAPSHOT_BUILD: 'Build Snapshot', - SNAPSHOT_REFRESH: 'Refresh Snapshot', + LAYOUT_DATA_OPTIMIZE: 'Storage Optimization', clearUpIntermediateTable: 'Garbage CleanUp', project: 'Project', adminTips: 'Admin user can view all job information via Select All option in the project list.', @@ -119,6 +119,10 @@ export default { sampleTableData: 'Sample Table Data', buildIndexesByLayer: 'Build indexes by layer', updateFlatTableStatistics: 'Update flat table statistics', + deleteUselessLayoutData: 'Delete invalid data', + optimizeLayoutDataByRepartition: 'Index data repartition', + optimizeLayoutDataByZorder: 'Z-order sorting', + optimizeLayoutDataByCompaction: 'Modify index file size', durationStart: 'Start Time: ', durationEnd: 'End Time: ', jobParams: 'Job Parameters', @@ -139,6 +143,8 @@ export default { step_stopped: 'Paused', step_skip: 'Skipped', errorStepTips: 'Job interrupted, "{name}" step error', - noErrorMsg: 'No error message' + noErrorMsg: 'No error message', + chRestartTips: 'Can\'t restart the current job where some data has been loaded.', + fullOptimization: 'Full Optimization' } } diff --git a/kystudio/src/components/query/query_history_table.vue b/kystudio/src/components/query/query_history_table.vue index 21e5533d69..35709e38e5 100644 --- a/kystudio/src/components/query/query_history_table.vue +++ b/kystudio/src/components/query/query_history_table.vue @@ -26,7 +26,7 @@ </el-dropdown> </div> </div> - </div> + </div> <div class="clearfix ksd-mb-16"> <div class="table-filters ksd-fleft"> @@ -355,7 +355,13 @@ </template> </el-table-column> </el-table> - <index-details :index-detail-title="indexDetailTitle" :detail-type="detailType" :cuboid-data="cuboidData" @close="closeDetailDialog" v-if="indexDetailShow" /> + <index-details + :index-detail-title="indexDetailTitle" + :detail-type="detailType" + :cuboid-data="cuboidData" + :storage-type="cuboidDataStorageType" + @close="closeDetailDialog" + v-if="indexDetailShow" /> <diagnostic v-if="showDiagnostic" @close="showDiagnostic = false" @@ -510,6 +516,7 @@ export default class QueryHistoryTable extends Vue { isShowDetail_PREPARATION = false // 展开查询步骤详情 isShowDetail_JOB_EXECUTION = false // 展开spark任务执行步骤详情 cuboidData = {} + cuboidDataStorageType = 1 indexDetailTitle = '' indexDetailShow = false detailType = '' @@ -1043,7 +1050,8 @@ export default class QueryHistoryTable extends Vue { // 展示 layout 详情 async openLayoutDetails (item) { if (!item.layoutExist) return - const {modelId, layoutId} = item + const { modelId, layoutId, storageType } = item + this.cuboidDataStorageType = storageType try { const res = await this.loadAllIndex({ project: this.currentSelectedProject, diff --git a/kystudio/src/components/studio/StudioModel/IndexOptimizationModal/index.vue b/kystudio/src/components/studio/StudioModel/IndexOptimizationModal/index.vue new file mode 100644 index 0000000000..7fccaaadb2 --- /dev/null +++ b/kystudio/src/components/studio/StudioModel/IndexOptimizationModal/index.vue @@ -0,0 +1,199 @@ +<template> + <el-dialog + class="index-optimization-modal" + append-to-body + :close-on-press-escape="false" + :close-on-click-modal="false" + :title="$t('indexOptimization')" + width="600px" + :visible="isShow" + @close="closeModal(false)"> + <div> + <p>{{$t('content1')}}<a href="javascript:;">{{$t('content2')}}</a>{{$t('content3')}}<a href="javascript:;" @click="downloadTemplate">{{$t('content4')}}</a>{{$t('content5')}}</p> + <div class="aceEditorBox"> + <AceEditor + v-if="isShow" + :lang="'json'" + :placeholder="'123'" + class="text-input" + ref="editorRef" + :value="codeText" + @input="handleInputText" /> + </div> + <div class="error-msg" v-if="hasChecked && errMsg">{{errMsg}}</div> + <form name="download" class="downloadTemplate" :action="actionUrl" target="_blank" method="get"> + </form> + </div> + <div slot="footer" class="dialog-footer ky-no-br-space"> + <el-button @click="closeModal(false)" size="medium">{{$t('kylinLang.common.cancel')}}</el-button> + <el-button type="primary" @click="submit" :loading="btnLoading" size="medium">{{$t('kylinLang.common.submit')}}</el-button> + </div> + </el-dialog> +</template> +<script> +import Vue from 'vue' +import AceEditor from 'vue2-ace-editor' +import { Component } from 'vue-property-decorator' +import { mapState, mapMutations, mapActions, mapGetters } from 'vuex' +import vuex from '../../../../store' +import locales from './locales' +import store, { types } from './store' +import { handleError, kylinMessage } from 'util/business' +import { apiUrl } from '../../../../config' +vuex.registerModule(['modals', 'IndexOptimizationModal'], store) +@Component({ + computed: { + ...mapGetters(['currentSelectedProject']), + ...mapState('IndexOptimizationModal', { + isShow: state => state.isShow, + callback: state => state.callback, + modelInstance: state => state.modelInstance + }) + }, + methods: { + ...mapActions({ + saveOptimizeLayoutData: 'SAVE_OPTIMIZE_LAYOUT_DATA', + downloadOptimizeLayoutTemplate: 'DOWNLOAD_OPTIMIZE_LAYOUT_TEMPLATE' + }), + ...mapMutations('IndexOptimizationModal', { + setModal: types.SET_MODAL, + hideModal: types.HIDE_MODAL, + setModalForm: types.SET_MODAL_FORM, + resetModalForm: types.RESET_MODAL_FORM + }) + }, + components: { + AceEditor + }, + locales +}) +export default class IndexOptimizationModal extends Vue { + btnLoading = false + codeText = '' + jsonResut = {} + hasChecked = false + errMsg = '' + + get actionUrl () { + return `${apiUrl}models/optimize_layout_data_template` + } + + downloadTemplate () { + if (this.$store.state.config.platform === 'iframe') { // 云上暂不支持流数据,暂不处理 + try { + this.downloadOptimizeLayoutTemplate().then(res => { + const file = (res && res.headers && res.headers.map && res.headers.map['content-disposition'] && res.headers.map['content-disposition'][0]) || '' + const fileNameStrArr = file.split(';') + const fileNameArr = fileNameStrArr[1] ? fileNameStrArr[1].split('=') : [] + if (res && res.status === 200 && res.body) { + // 动态从header 里取文件名,如果没取到,就前端自己配 + const fileName = fileNameArr[1].trim().replace(/"/g, '') || 'optimize_layout_data_template.json' + const data = JSON.stringify(res.body, null, 2) + const blob = new Blob([data], { type: 'application/json;charset=utf-8' }) + if (window.navigator.msSaveOrOpenBlob) { + navigator.msSaveBlob(blob, fileName) + } else { + const link = document.createElement('a') + link.href = window.URL.createObjectURL(blob) + link.download = fileName + link.click() + window.URL.revokeObjectURL(link.href) + } + } + }) + } catch (e) { + console.log(e) + } + } else { + this.$nextTick(() => { + this.$el.querySelectorAll('.downloadTemplate').length && this.$el.querySelectorAll('.downloadTemplate')[0].submit() + }) + } + } + + handleValidate (text) { + if (!text || !text.trim()) { + this.errMsg = this.$t('notEmpty') + } else { + try { + this.jsonResut = JSON.parse(text) + this.errMsg = '' + } catch (e) { + this.jsonResut = {} + this.errMsg = this.$t('formatErrMsg') + console.log(e) + } + } + } + + handleInputText (text) { + this.codeText = text + this.handleValidate(text) + } + + handleResetInfo () { + this.codeText = '' + this.btnLoading = false + this.jsonResut = {} + this.errMsg = '' + this.hasChecked = false + } + + closeModal (isSubmit) { + this.handleResetInfo() + this.hideModal() + setTimeout(() => { + this.callback && this.callback({ isSubmit }) + this.resetModalForm() + }, 200) + } + + async submit () { + // 标记已经点过提交按钮了 + this.hasChecked = true + this.handleValidate(this.codeText) + if (this.errMsg) { + return false + } + this.btnLoading = true + try { + // 接口参数中的数据来自文本输入框中的内容 json 化 + await this.saveOptimizeLayoutData({ + project: this.currentSelectedProject, + model_id: this.modelInstance.uuid, + ...this.jsonResut + }) + this.btnLoading = false + kylinMessage(this.$t('submitSuccess')) + this.closeModal(true) + } catch (res) { + this.btnLoading = false + res && handleError(res) + } + } +} +</script> +<style lang="less"> +@import '../../../../assets/styles/variables.less'; +.index-optimization-modal{ + .aceEditorBox{ + height: 300px; + margin-top:20px; + } + .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-placeholder-color; + padding: 0 6px; + white-space: pre-wrap; + } + } +} + +</style> diff --git a/kystudio/src/components/studio/StudioModel/IndexOptimizationModal/locales.js b/kystudio/src/components/studio/StudioModel/IndexOptimizationModal/locales.js new file mode 100644 index 0000000000..2b86fa9fde --- /dev/null +++ b/kystudio/src/components/studio/StudioModel/IndexOptimizationModal/locales.js @@ -0,0 +1,28 @@ +export default { + en: { + indexOptimization: 'Storage Optimization', + content1: 'You can optimize the storage structure by setting index partition columns, Z-order column, etc. based on data characteristics and query modes to help improve query performance. Please modify it with caution, unreasonable settings may cause query performance to decrease. View the ', + content2: 'manual', + content3: ' or download the ', + content4: 'template', + content5: '.', + submitSuccess: 'Submitted successfully. You may go to the job page to check details', + formatErrMsg: 'Format error, please modify and try again.', + columnNotExist: '{columns} does not exist, please modify and try again.', + indexInProgress: 'The storage optimization of {index} is in progress and can\'t be submitted again. Please modify and try again.', + notEmpty: 'Can not be empty' + }, + 'zh-cn': { + indexOptimization: '存储优化', + content1: '您可以根据数据特征和查询模式,通过设置索引分区列、Z-order 列等方式优化存储结构,帮助提升查询性能。请谨慎修改,不合理的设置可能导致查询性能下降。查看', + content2: '手册', + content3: '或下载', + content4: '模板', + content5: '。', + submitSuccess: '已提交,您可以到任务页查看进度', + formatErrMsg: '格式错误,请修改后重试。', + columnNotExist: '{columns}不存在,请修改后重试。', + indexInProgress: '{index} 正在进行存储优化,无法重复提交,请修改后重试。', + notEmpty: '内容不能为空' + } +} diff --git a/kystudio/src/components/studio/StudioModel/IndexOptimizationModal/store.js b/kystudio/src/components/studio/StudioModel/IndexOptimizationModal/store.js new file mode 100644 index 0000000000..b186637f6a --- /dev/null +++ b/kystudio/src/components/studio/StudioModel/IndexOptimizationModal/store.js @@ -0,0 +1,44 @@ +const types = { + SHOW_MODAL: 'SHOW_MODAL', + HIDE_MODAL: 'HIDE_MODAL', + SET_MODAL_FORM: 'SET_MODAL_FORM', + RESET_MODAL_FORM: 'RESET_MODAL_FORM', + CALL_MODAL: 'CALL_MODAL' +} +// 声明:初始state状态 +const initialState = JSON.stringify({ + isShow: false, + callback: null, + modelInstance: null +}) +export default { + // state深拷贝 + state: JSON.parse(initialState), + mutations: { + // 显示Modal弹窗 + [types.SHOW_MODAL]: state => { + state.isShow = true + }, + // 隐藏Modal弹窗 + [types.HIDE_MODAL]: state => { + state.isShow = false + }, + [types.SET_MODAL_FORM]: (state, payload) => { + state.callback = payload.callback + state.modelInstance = payload.modelInstance + }, + [types.RESET_MODAL_FORM]: state => { + state.form = JSON.parse(initialState).form + } + }, + actions: { + [types.CALL_MODAL] ({ commit }, data) { + return new Promise(resolve => { + commit(types.SET_MODAL_FORM, { callback: resolve, modelInstance: data.modelInstance || null }) + commit(types.SHOW_MODAL) + }) + } + }, + namespaced: true +} +export { types } diff --git a/kystudio/src/components/studio/StudioModel/ModelList/AggregateModal/index.vue b/kystudio/src/components/studio/StudioModel/ModelList/AggregateModal/index.vue index ef4b60475e..b413638f51 100644 --- a/kystudio/src/components/studio/StudioModel/ModelList/AggregateModal/index.vue +++ b/kystudio/src/components/studio/StudioModel/ModelList/AggregateModal/index.vue @@ -504,6 +504,7 @@ :rightTitleTip="$t('includeDimTips')" :isEdit="selectedIncludeDimension.length>0" :alertTips="alertTips" + :showShardBy="!isStorageV3" @setSelectedColumns="(v) => setSelectedColumns(v)" @setShardbyCol="(label) => setShardbyCol(label)"> <template slot="left-footer" v-if="showExcludedTableCheckBox"> @@ -795,6 +796,11 @@ export default class AggregateModal extends Vue { return flag } + // 当前模型的存储版本 + get isStorageV3 () { + return this.model.storage_type === 3 + } + get onlyRealTimeType () { return (this.model.model_type === 'HYBRID' && this.form.aggregateArray.filter(item => item.index_range === 'STREAMING').length === this.form.aggregateArray.length) || this.model.model_type === 'STREAMING' } diff --git a/kystudio/src/components/studio/StudioModel/ModelList/IndexDetailModal/index.vue b/kystudio/src/components/studio/StudioModel/ModelList/IndexDetailModal/index.vue new file mode 100644 index 0000000000..8cfc3dcffa --- /dev/null +++ b/kystudio/src/components/studio/StudioModel/ModelList/IndexDetailModal/index.vue @@ -0,0 +1,122 @@ +<template> + <el-dialog + width="720px" + class="index-detail-modal" + :title="$t('dialogType')" + :append-to-body="true" + :visible="isShow" + @close="closeModal" + :close-on-click-modal="false" + :close-on-press-escape="false"> + <div class="ksd-list info-detail"> + <p class="list"> + <span class="label">Index ID</span> + <span class="text">{{infoDetail.id}}</span> + </p> + <p class="list"> + <span class="label">{{$t('indexPath')}}</span> + <span class="text path">{{indexDetail.location || '-'}}</span> + </p> + <p class="list"> + <span class="label">{{$t('fileCount')}}</span> + <span class="text">{{indexDetail.num_of_files || '-'}}</span> + </p> + <p class="list"> + <span class="label">{{$t('patitionColumn')}}</span> + <span class="text">{{indexDetail.partition_columns ? indexDetail.partition_columns.join(',') : '-'}}</span> + </p> + <p class="list"> + <span class="label">Z-order By</span> + <span class="text">{{indexDetail.zorder_by_columns ? indexDetail.zorder_by_columns.join(',') : '-'}}</span> + </p> + <p class="list"> + <span class="label">{{$t('maximumFileSize')}}</span> + <span class="text">{{indexDetail.max_compaction_file_size_in_bytes | dataSize}}</span> + </p> + <p class="list"> + <span class="label">{{$t('minimumFileSize')}}</span> + <span class="text">{{indexDetail.min_compaction_file_size_in_bytes | dataSize}}</span> + </p> + </div> + <div slot="footer" class="dialog-footer ky-no-br-space"> + <el-button + size="medium" + @click="closeModal">{{$t('kylinLang.common.close')}}</el-button> + </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 locales from './locales' +import store, { types } from './store' +import { handleError, handleSuccessAsync } from '../../../../../util' +vuex.registerModule(['modals', 'IndexDetailModal'], store) +@Component({ + computed: { + ...mapGetters(['currentSelectedProject']), + ...mapState('IndexDetailModal', { + isShow: state => state.isShow, + callback: state => state.callback, + infoDetail: state => state.infoDetail + }) + }, + methods: { + ...mapActions({ + getIndexDetail: 'GET_INDEX_DETAIL' + }), + ...mapMutations('IndexDetailModal', { + hideModal: types.HIDE_MODAL + }) + }, + components: { + }, + locales +}) +export default class IndexDetailModal extends Vue { + indexDetail = {} + + @Watch('isShow') + async onModalShow (val) { + if (val) { + try { + const res = await this.getIndexDetail({ + project: this.currentSelectedProject, + model_id: this.infoDetail.model, + index_id: this.infoDetail.id + }) + const result = await handleSuccessAsync(res) + if (result) { + this.indexDetail = result + } + } catch (e) { + console.log(e) + handleError(e) + } + } + } + + closeModal () { + this.hideModal() + } +} +</script> +<style lang="less"> +.index-detail-modal{ + .info-detail{ + .label { + width:130px; + text-align: right; + } + .path { + word-break: break-all; + } + &.ksd-list .list { + align-items: baseline; + } + } +} + +</style> diff --git a/kystudio/src/components/studio/StudioModel/ModelList/IndexDetailModal/locales.js b/kystudio/src/components/studio/StudioModel/ModelList/IndexDetailModal/locales.js new file mode 100644 index 0000000000..d090883a18 --- /dev/null +++ b/kystudio/src/components/studio/StudioModel/ModelList/IndexDetailModal/locales.js @@ -0,0 +1,34 @@ +export default { + en: { + dialogType: 'Index Details', + indexPath: 'Path', + fileCount: 'File Number', + patitionColumn: 'Patition Column', + customSetting: 'Custom Settings', + storage: 'Storage', + CUSTOM_AGG_INDEX: 'Custom Aggregate Group', + RECOMMENDED_AGG_INDEX: 'Recommended Aggregate Index', + CUSTOM_TABLE_INDEX: 'Custom Table Index', + RECOMMENDED_TABLE_INDEX: 'Recommended Table Index', + BASE_AGG_INDEX: 'Base Aggregate Index', + BASE_TABLE_INDEX: 'Base Table Index', + maximumFileSize: 'Maximum file size', + minimumFileSize: 'Minimum file size' + }, + 'zh-cn': { + dialogType: '索引详情', + indexPath: '路径', + fileCount: '文件数', + patitionColumn: '分区列', + customSetting: '自定义设置', + storage: '存储空间', + CUSTOM_AGG_INDEX: '自定义聚合索引', + RECOMMENDED_AGG_INDEX: '系统推荐聚合索引', + CUSTOM_TABLE_INDEX: '自定义明细索引', + RECOMMENDED_TABLE_INDEX: '系统推荐明细索引', + BASE_TABLE_INDEX: '基础明细索引', + BASE_AGG_INDEX: '基础聚合索引', + maximumFileSize: '文件大小上限', + minimumFileSize: '文件大小下限' + } +} diff --git a/kystudio/src/components/studio/StudioModel/ModelList/IndexDetailModal/store.js b/kystudio/src/components/studio/StudioModel/ModelList/IndexDetailModal/store.js new file mode 100644 index 0000000000..f5246d9b93 --- /dev/null +++ b/kystudio/src/components/studio/StudioModel/ModelList/IndexDetailModal/store.js @@ -0,0 +1,44 @@ +const types = { + SHOW_MODAL: 'SHOW_MODAL', + HIDE_MODAL: 'HIDE_MODAL', + SET_MODAL_FORM: 'SET_MODAL_FORM', + RESET_MODAL_FORM: 'RESET_MODAL_FORM', + CALL_MODAL: 'CALL_MODAL' +} +// 声明:初始state状态 +const initialState = JSON.stringify({ + isShow: false, + callback: null, + infoDetail: {} +}) +export default { + // state深拷贝 + state: JSON.parse(initialState), + mutations: { + // 显示Modal弹窗 + [types.SHOW_MODAL]: state => { + state.isShow = true + }, + // 隐藏Modal弹窗 + [types.HIDE_MODAL]: state => { + state.isShow = false + }, + [types.SET_MODAL_FORM]: (state, payload) => { + state.callback = payload.callback + state.infoDetail = payload.infoDetail + }, + [types.RESET_MODAL_FORM]: state => { + state.infoDetail = JSON.parse(initialState).infoDetail + } + }, + actions: { + [types.CALL_MODAL] ({ commit }, data) { + return new Promise(resolve => { + commit(types.SET_MODAL_FORM, { callback: resolve, infoDetail: data }) + commit(types.SHOW_MODAL) + }) + } + }, + namespaced: true +} +export { types } diff --git a/kystudio/src/components/studio/StudioModel/ModelList/ModelActions/locales.js b/kystudio/src/components/studio/StudioModel/ModelList/ModelActions/locales.js index e10cab5d81..efda2cb4f0 100644 --- a/kystudio/src/components/studio/StudioModel/ModelList/ModelActions/locales.js +++ b/kystudio/src/components/studio/StudioModel/ModelList/ModelActions/locales.js @@ -3,6 +3,7 @@ export default { authorityDetails: 'The details of authority', iKnow: 'Got it', exportTds: 'Export TDS', + storageSetting: 'Storage Setting', rename: 'Rename', delete: 'Delete', purge: 'Purge', @@ -28,6 +29,7 @@ export default { exportMetadatas: 'Export Model', bokenModelExportTDSTip: 'Can\'t export TDS file at the moment as the model is BROKEN', bokenModelExportMetadatasTip: 'Can\'t export model file at the moment as the model is BROKEN', + disableModelStorageSettingTip: 'Can\'t change the storage type. The storage type can only be changed when the model data is empty and has no running build jobs.', buildTips: 'The indexes in this model have not been built and are not ready for serving queries. Please build indexes to optimize query performance.', multilParTip: 'This model used multilevel partitioning, which are not supported at the moment. Please set subpartition as \'None\' in model partition dialog, or turn on \'Multilevel Partitioning\' in project settings.', change: 'Change', diff --git a/kystudio/src/components/studio/StudioModel/ModelList/ModelActions/modelActions.vue b/kystudio/src/components/studio/StudioModel/ModelList/ModelActions/modelActions.vue index 544cf9bdca..5a1439873d 100644 --- a/kystudio/src/components/studio/StudioModel/ModelList/ModelActions/modelActions.vue +++ b/kystudio/src/components/studio/StudioModel/ModelList/ModelActions/modelActions.vue @@ -95,6 +95,16 @@ <span>{{$t('exportTds')}}</span> </common-tip> </el-dropdown-item> + <el-dropdown-item + v-if="modelActions.includes('storageSetting')" + command="storageSetting" + :class="{'disabled-action': disableStorageSetting(this.currentModel)}"> + <common-tip + :content="$t('disableModelStorageSettingTip')" + :disabled="!disableStorageSetting(this.currentModel)"> + <span>{{$t('storageSetting')}}</span> + </common-tip> + </el-dropdown-item> <el-dropdown-item command="rename" divided @@ -212,7 +222,8 @@ <el-button type="primary" size="medium" @click="handlerExportTDS">{{$t('kylinLang.query.export')}}</el-button> </div> </el-dialog> - + <!-- 存储设置 --> + <SettingStorageModal /> </div> </template> @@ -224,6 +235,7 @@ import { handleSuccessAsync, objectClone } from 'util' import { apiUrl } from '../../../../../config' import { handleError, kylinConfirm, kylinMessage, handleSuccess } from 'util/business' import locales from './locales' +import SettingStorageModal from '../../SettingStorageModal' @Component({ props: { @@ -254,6 +266,9 @@ import locales from './locales' default: false } }, + components: { + SettingStorageModal + }, computed: { ...mapGetters([ 'datasourceActions', @@ -298,6 +313,9 @@ import locales from './locales' }), ...mapActions('ModelsExportModal', { callModelsExportModal: 'CALL_MODAL' + }), + ...mapActions('SettingStorageModal', { + callSettingStorageModal: 'CALL_MODAL' }) }, locales @@ -333,6 +351,15 @@ export default class ModelActions extends Vue { {value: 'TABLEAU_CONNECTOR_TDS', text: 'connectTableau'} ] + // 禁用存储设置 + disableStorageSetting (modelDesc) { + // 模型数据不为空 + const modelHasData = modelDesc.has_segments + // 或者模型状态为 broken + const modelIsBroken = modelDesc.status === 'BROKEN' + return modelHasData || modelIsBroken + } + // 处理每行的构建按钮的 disable 状态 disableLineBuildBtn (row) { if (row.model_type === 'STREAMING') { @@ -505,6 +532,9 @@ export default class ModelActions extends Vue { case 'purge': flag = modelDesc.model_type !== 'HYBRID' break + case 'storageSetting': + flag = !this.disableStorageSetting(modelDesc) + break default: flag = true break @@ -598,6 +628,12 @@ export default class ModelActions extends Vue { this.showExportTDSDialog = true this.currentExportTDSModel = modelDesc } + } else if (command === 'storageSetting') { + const { isSubmit } = await this.callSettingStorageModal(modelDesc) + if (isSubmit) { + // 刷新模型当前页面 + this.$emit('loadModelsList') + } } } diff --git a/kystudio/src/components/studio/StudioModel/ModelList/ModelAggregate/index.vue b/kystudio/src/components/studio/StudioModel/ModelList/ModelAggregate/index.vue index 8e6b0e09a3..5a40a766ec 100644 --- a/kystudio/src/components/studio/StudioModel/ModelList/ModelAggregate/index.vue +++ b/kystudio/src/components/studio/StudioModel/ModelList/ModelAggregate/index.vue @@ -103,6 +103,15 @@ </el-dropdown-menu> </el-dropdown> </template> + <!-- 存储优化按钮 --> + <el-button + v-if="isStorageV3 && modelActions.includes('indexOptimization')" + icon="el-ksd-n-icon-setting-outlined" + text type="primary" + class="ksd-ml-2 ksd-fleft" + @click="handleShowIndexOptimizationDialog"> + {{$t('indexOptimization')}} + </el-button> <div class="right fix"> <el-input class="search-input" v-model.trim="filterArgs.key" size="medium" :placeholder="$t('searchAggregateID')" prefix-icon="el-ksd-icon-search_22" v-global-key-event.enter.debounce="searchAggs" @clear="searchAggs()"></el-input> </div> @@ -176,6 +185,9 @@ <common-tip :content="$t('editIndex')" v-if="isShowAggregateAction&&datasourceActions.includes('editAggGroup')"> <i class="el-icon-ksd-table_edit ksd-ml-5" v-if="scope.row.source === 'MANUAL_TABLE'" @click="confrimEditTableIndex(scope.row)"></i> </common-tip> + <common-tip :content="$t('viewDetail')" v-if="isStorageV3"> + <i class="el-ksd-icon-view_22 ksd-ml-5" @click="handleShowIndexDetail(scope.row)"></i> + </common-tip> <common-tip :content="$t('update')" v-if="scope.row.need_update"> <i class="action-icons el-ksd-icon-refresh_22 ksd-ml-5" @click="updateBaseIndexEvent(scope.row)"></i> </common-tip> @@ -188,7 +200,15 @@ </el-card> </div> </div> - <index-details :indexDetailTitle="indexDetailTitle" :detailType="detailType" :cuboidData="cuboidData" @close="closeDetailDialog" v-if="indexDetailShow" /> + <index-details + :indexDetailTitle="indexDetailTitle" + :detailType="detailType" + :cuboidData="cuboidData" + :storageType="model.storage_type" + @close="closeDetailDialog" + v-if="indexDetailShow" /> + <IndexOptimizationModal /> + <IndexDetailModal /> </div> </template> @@ -204,6 +224,8 @@ import { BuildIndexStatus } from '../../../../../config/model' import { formatGraphData } from './handler' import NModel from '../../ModelEdit/model.js' import IndexDetails from './indexDetails' +import IndexOptimizationModal from '../../IndexOptimizationModal' +import IndexDetailModal from '../IndexDetailModal' @Component({ props: { @@ -245,7 +267,8 @@ import IndexDetails from './indexDetails' ...mapGetters([ 'currentProjectData', 'datasourceActions', - 'isOnlyQueryNode' + 'isOnlyQueryNode', + 'modelActions' ]), modelInstance () { this.model.project = this.currentProjectData.name @@ -262,6 +285,12 @@ import IndexDetails from './indexDetails' ...mapActions('TableIndexEditModal', { showTableIndexEditModal: 'CALL_MODAL' }), + ...mapActions('IndexOptimizationModal', { + callIndexOptimizationModal: 'CALL_MODAL' + }), + ...mapActions('IndexDetailModal', { + callIndexDetailModal: 'CALL_MODAL' + }), ...mapActions({ fetchIndexGraph: 'FETCH_INDEX_GRAPH', buildIndex: 'BUILD_INDEX', @@ -278,7 +307,9 @@ import IndexDetails from './indexDetails' }) }, components: { - IndexDetails + IndexDetails, + IndexOptimizationModal, + IndexDetailModal }, locales }) @@ -332,11 +363,29 @@ export default class ModelAggregate extends Vue { return this.switchModelType === 'BATCH' ? this.model.batch_id : this.model.uuid } } + // 当前模型的存储版本 + get isStorageV3 () { + return this.model.storage_type === 3 + } get isHaveDelIndexActionSpec () { return this.datasourceActions.includes('delAggIdx') || this.isAdvancedOperatorUser() } + async handleShowIndexDetail (row) { + await this.callIndexDetailModal(row) + } + + // 打开索引优化的弹窗 + async handleShowIndexOptimizationDialog () { + const { isSubmit } = await this.callIndexOptimizationModal({ + modelInstance: this.model + }) + if (isSubmit) { + this.$emit('refreshModel') + } + } + formatDataSize (dataSize) { const [size = +size, ext] = this.$root.$options.filters.dataSize(dataSize).split(' ') const intType = ['B', 'KB'] diff --git a/kystudio/src/components/studio/StudioModel/ModelList/ModelAggregate/indexDetails.vue b/kystudio/src/components/studio/StudioModel/ModelList/ModelAggregate/indexDetails.vue index 5fdc9c0015..1fc40fe10f 100644 --- a/kystudio/src/components/studio/StudioModel/ModelList/ModelAggregate/indexDetails.vue +++ b/kystudio/src/components/studio/StudioModel/ModelList/ModelAggregate/indexDetails.vue @@ -36,6 +36,7 @@ </template> </el-table-column> <el-table-column + v-if="showShardby" label="ShardBy" align="center" width="80"> @@ -72,6 +73,7 @@ </template> </el-table-column> <el-table-column + v-if="showShardby" label="ShardBy" align="center" width="80"> @@ -97,6 +99,10 @@ import { transToGmtTime } from '../../../../../util/business' @Component({ props: { + storageType: { + type: Number, + default: 1 + }, indexDetailTitle: { type: String, default: '' @@ -122,6 +128,10 @@ export default class indexDetails extends Vue { currentAggPage = 0 totalAggIndexColumnSize = 0 + get showShardby () { + return this.storageType !== 3 + } + get cuboidDetail () { if (!this.cuboidData || !this.cuboidData.col_order || this.detailType === 'tabelIndexDetail') { return [] diff --git a/kystudio/src/components/studio/StudioModel/ModelList/ModelAggregate/locales.js b/kystudio/src/components/studio/StudioModel/ModelList/ModelAggregate/locales.js index e6c15b6352..90c511a25b 100644 --- a/kystudio/src/components/studio/StudioModel/ModelList/ModelAggregate/locales.js +++ b/kystudio/src/components/studio/StudioModel/ModelList/ModelAggregate/locales.js @@ -85,6 +85,7 @@ export default { disabledDelBaseIndexTips: 'To delete the base table index, please turn off tiered storage for the model.', refuseAddIndexTip: 'Can\'t add streaming indexes. Please stop the streaming job and then delete all the streaming segments.', refuseRemoveIndexTip: 'Can\'t delete streaming indexes. Please stop the streaming job and then delete all the streaming segments.', - unCreateBaseAggIndexNoDimensionTip: 'Can\'t add the base table index, as there are no dimensions in the model. Please add and try again.' + unCreateBaseAggIndexNoDimensionTip: 'Can\'t add the base table index, as there are no dimensions in the model. Please add and try again.', + indexOptimization: 'Storage Optimization' } } diff --git a/kystudio/src/components/studio/StudioModel/ModelList/ModelAggregateView/index.vue b/kystudio/src/components/studio/StudioModel/ModelList/ModelAggregateView/index.vue index 758558f3fc..d84db010fa 100644 --- a/kystudio/src/components/studio/StudioModel/ModelList/ModelAggregateView/index.vue +++ b/kystudio/src/components/studio/StudioModel/ModelList/ModelAggregateView/index.vue @@ -13,7 +13,7 @@ <div class="icon-group ksd-fright" v-if="isShowEditAgg"> <common-tip :content="!editOrAddIndex ? $t('refuseAddIndexTip') : $t('addAggGroup')"> <el-button text type="primary" :disabled="!editOrAddIndex" icon-button icon="el-ksd-icon-add_with_border_22" size="small" @click="(e) => handleAggregateGroup(e)"></el-button> - </common-tip><common-tip :content="$t('aggAdvanced')" v-if="model.model_type === 'BATCH'"> + </common-tip><common-tip :content="$t('aggAdvanced')" v-if="model.model_type === 'BATCH' && !isStorageV3"> <el-button text type="primary" icon-button icon="el-ksd-icon-setting_old" size="small" @click="openAggAdvancedModal()"></el-button> </common-tip> </div> @@ -215,6 +215,11 @@ export default class AggregateView extends Vue { return !(!this.indexUpdateEnabled && this.model.model_type === 'STREAMING') } + // 当前模型的存储版本 + get isStorageV3 () { + return this.model.storage_type === 3 + } + handleEditOrDelIndex (aggregate) { return !(!this.indexUpdateEnabled && (this.model.model_type === 'STREAMING' || ['HYBRID', 'STREAMING'].includes(aggregate.index_range))) } diff --git a/kystudio/src/components/studio/StudioModel/ModelList/ModelSegment/StreamingSegment/StreamingSegment.vue b/kystudio/src/components/studio/StudioModel/ModelList/ModelSegment/StreamingSegment/StreamingSegment.vue index edb6d1ea59..6338d8b73f 100644 --- a/kystudio/src/components/studio/StudioModel/ModelList/ModelSegment/StreamingSegment/StreamingSegment.vue +++ b/kystudio/src/components/studio/StudioModel/ModelList/ModelSegment/StreamingSegment/StreamingSegment.vue @@ -140,15 +140,15 @@ <span class="label">{{$t('segmentName')}}</span> <span class="text">{{detailSegment.name}}</span> </p> - <p class="list"> + <p class="list" v-if="!isStorageV3"> <span class="label">{{$t('segmentPath')}}</span> <span class="text segment-path">{{detailSegment.segmentPath}}</span> </p> - <p class="list"> + <p class="list" v-if="!isStorageV3"> <span class="label">{{$t('fileNumber')}}</span> <span class="text">{{detailSegment.fileNumber}}</span> </p> - <p class="list"> + <p class="list" v-if="!isStorageV3"> <span class="label">{{$t('storageSize1')}}</span> <span class="text">{{detailSegment.bytes_size | dataSize}}</span> </p> @@ -295,6 +295,10 @@ export default class StreamingSegment extends Vue { refreshLoading = false refreshType = 'refreshOrigin' + // 当前模型的存储版本 + get isStorageV3 () { + return this.model.storage_type === 3 + } get filterSegment () { return this.segments.filter(item => ['Full Load', '全量加载'].includes(item.startTime) && ['Full Load', '全量加载'].includes(item.endTime)).length } diff --git a/kystudio/src/components/studio/StudioModel/ModelList/ModelSegment/index.vue b/kystudio/src/components/studio/StudioModel/ModelList/ModelSegment/index.vue index baef85d310..c6e1d381ae 100644 --- a/kystudio/src/components/studio/StudioModel/ModelList/ModelSegment/index.vue +++ b/kystudio/src/components/studio/StudioModel/ModelList/ModelSegment/index.vue @@ -114,9 +114,21 @@ <span>{{scope.row.last_modified_time | toServerGMTDate}}</span> </template> </el-table-column> - <el-table-column :label="$t('sourceRecords')" width="150" align="right" prop="source_count" sortable="custom"> + <el-table-column + v-if="!isStorageV3" + :label="$t('sourceRecords')" + width="150" + align="right" + prop="source_count" + sortable="custom"> </el-table-column> - <el-table-column :label="$t('storageSize')" width="140" align="right" prop="storage" sortable="custom"> + <el-table-column + v-if="!isStorageV3" + :label="!model.second_storage_enabled ? $t('storageSize') : $store.state.config.platform !== 'iframe' ? $t('storageSizeForDFS') : $t('storageSizeForClound')" + width="220" + align="right" + prop="storage" + sortable="custom"> <template slot-scope="scope">{{scope.row.bytes_size | dataSize}}</template> </el-table-column> <el-table-column align="left" class-name="ky-hover-icon" fixed="right" :label="$t('kylinLang.common.action')" width="83"> @@ -153,15 +165,15 @@ <span class="label">{{$t('segmentName')}}</span> <span class="text">{{detailSegment.name}}</span> </p> - <p class="list"> + <p class="list" v-if="!isStorageV3"> <span class="label">{{$t('segmentPath')}}</span> <span class="text segment-path">{{detailSegment.segmentPath}}</span> </p> - <p class="list"> + <p class="list" v-if="!isStorageV3"> <span class="label">{{$t('fileNumber')}}</span> <span class="text">{{detailSegment.fileNumber}}</span> </p> - <p class="list"> + <p class="list" v-if="!isStorageV3"> <span class="label">{{$t('storageSize1')}}</span> <span class="text">{{detailSegment.bytes_size | dataSize}}</span> </p> @@ -514,6 +526,11 @@ export default class ModelSegment extends Vue { timestamp = Date.now().toString(32) showRefreshErrorTip = false + // 当前模型的存储版本 + get isStorageV3 () { + return this.model.storage_type === 3 + } + get getDetails () { let notices = [ { text: this.$t('mergeNotice1'), value: 1, isError: false }, diff --git a/kystudio/src/components/studio/StudioModel/ModelList/TableIndexView/index.vue b/kystudio/src/components/studio/StudioModel/ModelList/TableIndexView/index.vue index 0264dfeb15..15d9628619 100644 --- a/kystudio/src/components/studio/StudioModel/ModelList/TableIndexView/index.vue +++ b/kystudio/src/components/studio/StudioModel/ModelList/TableIndexView/index.vue @@ -73,6 +73,7 @@ </span> </el-table-column> <el-table-column + v-if="!isStorageV3" label="Shard" align="center" width="70"> @@ -156,6 +157,11 @@ export default class TableIndexView extends Vue { isLoading = false indexUpdateEnabled = true + // 当前模型的存储版本 + get isStorageV3 () { + return this.model.storage_type === 3 + } + handleEditOrDelIndex (index) { return !(!this.indexUpdateEnabled && (this.model.model_type === 'STREAMING' || ['HYBRID', 'STREAMING'].includes(index.index_range))) } diff --git a/kystudio/src/components/studio/StudioModel/SettingStorageModal/index.vue b/kystudio/src/components/studio/StudioModel/SettingStorageModal/index.vue new file mode 100644 index 0000000000..5ec17f27b4 --- /dev/null +++ b/kystudio/src/components/studio/StudioModel/SettingStorageModal/index.vue @@ -0,0 +1,130 @@ +<template> + <el-dialog + width="600px" + :title="$t('dialogType')" + class="storage_setting_dialog" + :append-to-body="true" + :visible="isShow" + @close="closeModal(false)" + :close-on-click-modal="false" + :close-on-press-escape="false"> + <div> + <el-form + :model="form" + @submit.native.prevent + ref="storageSettingForm" + @keyup.enter.native="submit"> + <el-form-item + :label="$t('storageType')" + prop="storage_type"> + <el-radio-group v-model="form.storage_type"> + <el-radio :label="1"> + {{$t('optionFixed')}} + <el-tooltip :content="$t('optionFixedTips')"> + <i class="el-ksd-n-icon-info-circle-outlined"></i> + </el-tooltip> + </el-radio> + <el-radio :label="3"> + {{$t('optionFlexiable')}} + <el-tooltip :content="$t('optionFlexiableTips')"> + <i class="el-ksd-n-icon-info-circle-outlined"></i> + </el-tooltip> + </el-radio> + </el-radio-group> + </el-form-item> + </el-form> + </div> + <div slot="footer" class="dialog-footer ky-no-br-space"> + <el-button + size="medium" + @click="closeModal(false)">{{$t('kylinLang.common.cancel')}}</el-button> + <el-button + :disabled="disableSubmitBtn" + type="primary" + size="medium" + @click="submit" + :loading="btnLoading">{{$t('kylinLang.common.save')}}</el-button> + </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 locales from './locales' +import store, { types } from './store' +import { handleError, kylinMessage } from 'util/business' +vuex.registerModule(['modals', 'SettingStorageModal'], store) +@Component({ + computed: { + ...mapGetters(['currentSelectedProject']), + ...mapState('SettingStorageModal', { + isShow: state => state.isShow, + callback: state => state.callback, + form: state => state.form + }) + }, + methods: { + ...mapActions({ + updateModelStorageType: 'UPDATE_STORAGE_TYPE' + }), + ...mapMutations('SettingStorageModal', { + setModal: types.SET_MODAL, + hideModal: types.HIDE_MODAL, + setModalForm: types.SET_MODAL_FORM, + resetModalForm: types.RESET_MODAL_FORM + }) + }, + components: { + }, + locales +}) +export default class SettingStorageModal extends Vue { + btnLoading = false + originType = '' + + @Watch('isShow') + onModalShow (val) { + if (val) { + this.originType = this.form.storage_type + } else { + this.originType = '' + } + } + + // 值没改动的时候,不让提交,以防 type 值从 0 变成了 1 + get disableSubmitBtn () { + return this.originType === this.form.storage_type + } + + closeModal (isSubmit) { + this.hideModal() + setTimeout(() => { + this.btnLoading = false + this.callback && this.callback({ isSubmit }) + this.resetModalForm() + }, 200) + } + + submit () { + this.btnLoading = true + this.updateModelStorageType({ + model_id: this.form.uuid, + data: { + project: this.currentSelectedProject, + storage_type: this.form.storage_type + } + }).then(() => { + this.btnLoading = false + kylinMessage(this.$t('updateSuccessful')) + this.closeModal(true) + }, (res) => { + this.btnLoading = false + res && handleError(res) + }) + } +} +</script> +<style lang="less"> +</style> diff --git a/kystudio/src/components/studio/StudioModel/SettingStorageModal/locales.js b/kystudio/src/components/studio/StudioModel/SettingStorageModal/locales.js new file mode 100644 index 0000000000..b1c6e68baf --- /dev/null +++ b/kystudio/src/components/studio/StudioModel/SettingStorageModal/locales.js @@ -0,0 +1,20 @@ +export default { + en: { + dialogType: 'Storage Setting', + storageType: 'Storage type', + optionFixed: 'Fixed partition storage', + optionFlexiable: 'Flexible partition storage', + optionFixedTips: 'Fixed partition storage must store data according to model partition settings and filter data in a fixed way to speed up queries.', + optionFlexiableTips: 'Flexible partition storage provides a more flexible storage partitioning method. You can set partition columns, Z-order columns, etc. at the index level based on data characteristics and query modes to help improve query performance in specific scenarios.', + updateSuccessful: 'Successfully updated' + }, + 'zh-cn': { + dialogType: '存储设置', + storageType: '存储类型', + optionFixed: '固定分区存储', + optionFlexiable: '灵活分区存储', + optionFixedTips: '固定分区存储必须按照模型分区设置存储数据,根据固定的方式过滤数据加速查询。', + optionFlexiableTips: '灵活分区存储提供更灵活的存储分区方式。您可以根据数据特征和查询模式,在索引级进行分区列、Z-order 列等设置,帮助提升特定场景的查询性能。', + updateSuccessful: '更新成功' + } +} diff --git a/kystudio/src/components/studio/StudioModel/SettingStorageModal/store.js b/kystudio/src/components/studio/StudioModel/SettingStorageModal/store.js new file mode 100644 index 0000000000..e23dc4501c --- /dev/null +++ b/kystudio/src/components/studio/StudioModel/SettingStorageModal/store.js @@ -0,0 +1,54 @@ +const types = { + SHOW_MODAL: 'SHOW_MODAL', + HIDE_MODAL: 'HIDE_MODAL', + SET_MODAL_FORM: 'SET_MODAL_FORM', + RESET_MODAL_FORM: 'RESET_MODAL_FORM', + CALL_MODAL: 'CALL_MODAL' +} +// 声明:初始state状态 +const initialState = JSON.stringify({ + isShow: false, + callback: null, + form: { + storage_type: 1, // 0 或 1 对应 v1 模型,3 对应 v3 模型 + uuid: '' // 模型 id + } +}) +export default { + // state深拷贝 + state: JSON.parse(initialState), + mutations: { + // 显示Modal弹窗 + [types.SHOW_MODAL]: state => { + state.isShow = true + }, + // 隐藏Modal弹窗 + [types.HIDE_MODAL]: state => { + state.isShow = false + }, + [types.SET_MODAL_FORM]: (state, payload) => { + state.callback = payload.callback + state.form = { + ...payload.form + } + }, + [types.RESET_MODAL_FORM]: state => { + state.form = JSON.parse(initialState).form + } + }, + actions: { + [types.CALL_MODAL] ({ commit }, data) { + return new Promise(resolve => { + // 统一 storage type 的值 + const temp = { + ...data, + storage_type: !data.storage_type || data.storage_type !== 3 ? 1 : data.storage_type + } + commit(types.SET_MODAL_FORM, { callback: resolve, form: temp }) + commit(types.SHOW_MODAL) + }) + } + }, + namespaced: true +} +export { types } diff --git a/kystudio/src/components/studio/StudioModel/TableIndexEdit/tableindex_edit.vue b/kystudio/src/components/studio/StudioModel/TableIndexEdit/tableindex_edit.vue index 32922c388c..6ed2b89571 100644 --- a/kystudio/src/components/studio/StudioModel/TableIndexEdit/tableindex_edit.vue +++ b/kystudio/src/components/studio/StudioModel/TableIndexEdit/tableindex_edit.vue @@ -16,6 +16,7 @@ isTextRecognition :isEdit="tableIndexMeta.id !== ''" :alertTips="alertTips" + :showShardBy="!isStorageV3" @handleTableIndexRecognize="handleTableIndexRecognize" @setSelectedColumns="(v) => setSelectedColumns(v)" @setShardbyCol="(label) => setShardbyCol(label)"> @@ -44,11 +45,11 @@ <i class="el-ksd-n-icon-close-L-outlined" @click="isShowHelp = false"></i> <div class="sugession-blocks"> <p> - <el-tag type="info" size="mini" is-light>{{ $t('sugessionLabel1') }}</el-tag> + <el-tag v-if="!isStorageV3" class="ksd-mr-8" ype="info" size="mini" is-light>{{ $t('sugessionLabel1') }}</el-tag> <span class="sugession">{{ $t('sugession1') }}</span> </p> - <p class="ksd-mt-16"> - <el-tag type="info" size="mini" is-light>{{ $t('sugessionLabel2') }}</el-tag> + <p class="ksd-mt-16" v-if="!isStorageV3"> + <el-tag class="ksd-mr-8" type="info" size="mini" is-light>{{ $t('sugessionLabel2') }}</el-tag> <span class="sugession">{{ $t('sugession2') }} <span class="tips">{{ $t('tips') }}<a class="ky-a-like" @click="goToDataSource">{{ $t('goToDataSource') }}</a></span> </span> @@ -170,10 +171,15 @@ return this.modelInstance.selected_columns.length ? this.modelInstance.selected_columns.filter(it => typeof it.excluded !== 'undefined' && it.excluded).length > 0 : false } + // 当前模型的存储版本 + get isStorageV3 () { + return this.modelInstance.storage_type === 3 + } + goToDataSource () { this.$router.push('/studio/source') } - + setSelectedColumns (selectedColumns) { this.selectedColumns = selectedColumns this.tableIndexMeta.col_order = [] @@ -538,7 +544,7 @@ display: flex; align-items: flex-start; .sugession { - margin-left: 8px; + //margin-left: 8px; line-height: 18px; } .tips { diff --git a/kystudio/src/config/spec.js b/kystudio/src/config/spec.js index e20c322b09..acb782e64d 100644 --- a/kystudio/src/config/spec.js +++ b/kystudio/src/config/spec.js @@ -95,7 +95,9 @@ export default { { "id": "offline" }, { "id": "online" }, { "id": "changeModelOwner" }, - { "id": "secStorageAction" } + { "id": "secStorageAction" }, + { "id": "indexOptimization" }, + { "id": "storageSetting" } ], "metadataActions": [ { "id": "executeModelMetadata" } @@ -198,9 +200,9 @@ export default { "modelActions": { "keyPattern": "groupRole-projectRole", "entries": [ - { "key": "systemAdmin-*", "value": "dataLoad,importMDX,exportTDS,exportMDX,rename,clone,delete,purge,offline,online,deleteIndex,changeModelOwner,manageSubPartitionValues,secStorageAction" }, - { "key": "systemUser-admin", "value": "dataLoad,importMDX,exportTDS,exportMDX,rename,clone,delete,purge,offline,online,deleteIndex,changeModelOwner,manageSubPartitionValues,secStorageAction" }, - { "key": "systemUser-management", "value": "dataLoad,importMDX,exportTDS,exportMDX,rename,clone,delete,purge,offline,online,deleteIndex,manageSubPartitionValues" }, + { "key": "systemAdmin-*", "value": "dataLoad,importMDX,exportTDS,exportMDX,rename,clone,delete,purge,offline,online,deleteIndex,changeModelOwner,manageSubPartitionValues,secStorageAction,indexOptimization,storageSetting" }, + { "key": "systemUser-admin", "value": "dataLoad,importMDX,exportTDS,exportMDX,rename,clone,delete,purge,offline,online,deleteIndex,changeModelOwner,manageSubPartitionValues,secStorageAction,indexOptimization,storageSetting" }, + { "key": "systemUser-management", "value": "dataLoad,importMDX,exportTDS,exportMDX,rename,clone,delete,purge,offline,online,deleteIndex,manageSubPartitionValues,indexOptimization" }, { "key": "systemUser-operation", "value": "purge,exportTDS,manageSubPartitionValues" }, { "key": "systemUser-read", "value": "exportTDS" } ] diff --git a/kystudio/src/service/model.js b/kystudio/src/service/model.js index cf8281bd59..9514d89c03 100644 --- a/kystudio/src/service/model.js +++ b/kystudio/src/service/model.js @@ -373,5 +373,32 @@ export default { }, validateExportTds: (para) => { return Vue.resource(apiUrl + 'models/validate_export?project=' + para.project + '&model=' + para.modelId + '&element=' + para.element).get() + }, + saveSecondaryStorageIndexes (para) { + return Vue.resource(apiUrl + 'storage/index').save(para) + }, + getSecondaryStorageIndexes (para) { + return Vue.resource(apiUrl + 'storage/index').get(para) + }, + materializeSecondaryStorageIndexes (para) { + return Vue.resource(apiUrl + 'storage/index/secondary/materialize').save(para) + }, + deleteSecondaryOrderByIndex (para) { + return Vue.resource(apiUrl + 'storage/index/primary').delete(para) + }, + deleteSecondarySkippingIndex (para) { + return Vue.resource(apiUrl + 'storage/index/secondary').delete(para) + }, + updateModelStorageType (para) { + return Vue.resource(apiUrl + `models/${para.model_id}/storage_type`).update(para.data) + }, + saveOptimizeLayoutData (para) { + return Vue.resource(apiUrl + `models/${para.model_id}/optimize_layout_data`).save(para) + }, + getIndexDetail (para) { + return Vue.resource(apiUrl + `models/${para.project}/${para.model_id}/${para.index_id}/layout_detail`).get() + }, + downloadOptimizeLayoutTemplate () { + return Vue.resource(apiUrl + 'models/optimize_layout_data_template').get() } } diff --git a/kystudio/src/store/model.js b/kystudio/src/store/model.js index 429e561209..092077c125 100644 --- a/kystudio/src/store/model.js +++ b/kystudio/src/store/model.js @@ -432,6 +432,18 @@ export default { }, [types.VALIDATE_EXPORT_TDS] (_, params) { return api.model.validateExportTds(params) + }, + [types.UPDATE_STORAGE_TYPE]: function ({ commit }, para) { + return api.model.updateModelStorageType(para) + }, + [types.SAVE_OPTIMIZE_LAYOUT_DATA]: function ({ commit }, para) { + return api.model.saveOptimizeLayoutData(para) + }, + [types.GET_INDEX_DETAIL]: function ({ commit }, para) { + return api.model.getIndexDetail(para) + }, + [types.DOWNLOAD_OPTIMIZE_LAYOUT_TEMPLATE]: function ({ commit }) { + return api.model.downloadOptimizeLayoutTemplate() } }, getters: { diff --git a/kystudio/src/store/types.js b/kystudio/src/store/types.js index 3722661906..5d7e7fc15b 100644 --- a/kystudio/src/store/types.js +++ b/kystudio/src/store/types.js @@ -300,6 +300,10 @@ export const VALIDATE_DATE_FORMAT = 'VALIDATE_DATE_FORMAT' export const CHECK_INTERNAL_MEASURE = 'CHECK_INTERNAL_MEASURE' export const UPDATE_FILTER_MODEL_NAME_CLOUD = 'UPDATE_FILTER_MODEL_NAME_CLOUD' export const VALIDATE_EXPORT_TDS = 'VALIDATE_EXPORT_TDS' +export const UPDATE_STORAGE_TYPE = 'UPDATE_STORAGE_TYPE' +export const SAVE_OPTIMIZE_LAYOUT_DATA = 'SAVE_OPTIMIZE_LAYOUT_DATA' +export const GET_INDEX_DETAIL = 'GET_INDEX_DETAIL' +export const DOWNLOAD_OPTIMIZE_LAYOUT_TEMPLATE = 'DOWNLOAD_OPTIMIZE_LAYOUT_TEMPLATE' // table index export const GET_TABLE_INDEX = 'GET_TABLE_INDEX' export const EDIT_TABLE_INDEX = 'EDIT_TABLE_INDEX'
