kystudio/src/components/studio/StudioModel/ModelList/index.vue (1,366 lines of code) (raw):

<template> <div class="mode-list" :class="{'full-cell': showFull}" id="modelListPage"> <div class="clearfix ksd-mt-32"> <div class="ksd-fleft"> <div class="ksd-title-page">{{$t('kylinLang.model.modelList')}}</div> </div> <div class="ksd-fright"> <div class="ky-no-br-space model-list-header"> <el-dropdown v-guide.addModelBtn split-button class="ksd-fleft" type="primary" size="medium" id="addModel" placement="bottom-start" btn-icon="el-ksd-icon-add_22" v-if="datasourceActions.includes('modelActions')" @click="showAddModelDialog">{{$t('kylinLang.common.new')}} <el-dropdown-menu slot="dropdown" class="model-actions-dropdown"> <el-dropdown-item v-if="$store.state.project.isSemiAutomatic && datasourceActions.includes('modelActions')" @click="showGenerateModelDialog"> {{$t('kylinLang.model.generateModel')}} </el-dropdown-item> <el-dropdown-item v-if="metadataActions.includes('executeModelMetadata')" @click="handleImportModels"> {{$t('importModels')}} </el-dropdown-item> </el-dropdown-menu> </el-dropdown> <common-tip :content="$t('noModelsExport')" v-if="metadataActions.includes('executeModelMetadata')" placement="top" :disabled="!!modelArray.length"> <el-button icon="el-ksd-icon-export_22" size="medium" class="ksd-ml-8" :disabled="!modelArray.length" @click="handleExportMetadatas"> <span>{{$t('exportMetadatas')}}</span> </el-button> </common-tip> </div> </div> </div> <div class="model-list-contain ksd-mt-16"> <!-- <div class="layout-mask" v-if="loadingModels"></div> --> <div class="clearfix"> <div class="table-filters ksd-fleft"> <DropdownFilter type="checkbox" trigger="click" :value="filterArgs.status" hideArrow @input="v => filterContent(v, 'status')" :options="[ { renderLabel: renderStatusLabel, value: 'ONLINE' }, { renderLabel: renderStatusLabel, value: 'OFFLINE' }, { renderLabel: renderStatusLabel, value: 'BROKEN' }, { renderLabel: renderStatusLabel, value: 'WARNING' }, ]"> <el-button text type="primary" iconr="el-ksd-icon-arrow_down_22">{{$t('status_c')}}{{selectedStatus.length > 1 ? `${selectedStatus[0]} +${selectedStatus.length - 1}` : selectedStatus.join('')}}</el-button> </DropdownFilter> <DropdownFilter class="ksd-ml-8" type="datetimerange" trigger="click" :value="filterArgs.last_modify" hideArrow :shortcuts="['lastDay', 'lastWeek', 'lastMonth']" @input="v => filterContent(v, 'last_modify')"> <el-button text type="primary" iconr="el-ksd-icon-arrow_down_22">{{$t('lastModifyTime_c')}}{{selectedRange}}</el-button> </DropdownFilter> <DropdownFilter class="ksd-ml-8" type="checkbox" trigger="click" hideArrow :value="filterArgs.model_attributes" :options="modelAttributesOptions" @input="v => filterContent(v, 'model_attributes')"> <el-button text type="primary" iconr="el-ksd-icon-arrow_down_22">{{$t('modelType_c')}}{{selectedModelAttributes.length > 1 ? `${selectedModelAttributes[0]} +${selectedModelAttributes.length - 1}` : selectedModelAttributes.join('')}}</el-button> </DropdownFilter> <div class="actions"> <el-button nobg-text class="reset-filters-btn" :disabled="isResetFilterDisabled" @click="handleResetFilters">{{$t('clearAll')}}</el-button> </div> </div> <div class="ksd-fright"> <el-input :placeholder="$t('filterModelOrOwner')" style="width:250px" size="medium" :prefix-icon="searchLoading? 'el-ksd-icon-loading_22':'el-ksd-icon-search_22'" :value="filterArgs.model_alias_or_owner" @input="handleFilterInput" v-global-key-event.enter.debounce="searchModels" @clear="searchModels()" class="show-search-btn" > </el-input> </div> </div> <p v-if="$store.state.config.platform === 'iframe'" class="ksd-mb-10"> <el-alert :title="$t('guideToAcceptRecom')" type="info" show-icon :closable="false"> </el-alert> </p> <el-table class="model_list_table" v-guide.scrollModelTable v-scroll-shadow :data="modelArray" :empty-text="emptyText" tooltip-effect="dark" v-loading="loadingModels" @row-click="modelRowClickEvent" :row-class-name="setRowClass" @sort-change="onSortChange" :cell-class-name="renderColumnClass" :expand-row-keys="expandedRows" :row-key="renderRowKey" @expand-change="expandRow" virtual-cell-height="64.57px" :scroll-ancestors="() => ['#scrollContent']" ref="modelListTable" style="width: 100%"> <el-table-column min-width="270px" prop="alias" :label="modelTableTitle"> <template slot-scope="scope"> <model-title-description :modelData="scope.row" @openSegment="openComplementSegment" @autoFix="autoFix" source="modelList" /> <!-- 工具栏 --> <model-actions :currentModel="scope.row" @loadModelsList="loadModelsList" @jump:recommendation="jumpToRecommendation"/> </template> </el-table-column> <el-table-column width="150px" sortable="custom" prop="recommendations_count" :label="$t('recommendationsTiTle')" v-if="$store.state.project.isSemiAutomatic && (datasourceActions.includes('accelerationActions') || isAdvancedOperatorUser())"> <template slot-scope="scope"> <template v-if="!(scope.row.status !== 'BROKEN' && ('visible' in scope.row && scope.row.visible)) || scope.row.model_type === 'HYBRID'">-</template> <el-tooltip effect="dark" :content="$t('recommendationsTiTle')" placement="bottom" v-else> <el-button type="primary" class="rec-btn" text icon="el-ksd-icon-wizard_22" @click.stop="jumpToRecommendation(scope.row)">{{scope.row.recommendations_count}}</el-button> </el-tooltip> </template> </el-table-column> <el-table-column :label="$t('kylinLang.common.fact')" width="180"> <template slot-scope="scope"> <template v-if="scope.row.status === 'BROKEN'">-</template> <template v-else> <el-popover :ref="`${scope.row.alias}-ERPopover`" placement="top-start" width="400" trigger="hover" @after-enter="(e) => afterPoppoverEnter(e, scope.row)" popper-class="er-popover-layout" > <div class="model-ER-layout"><ModelERDiagram v-if="scope.row.showER" ref="erDiagram" source="modelList" :show-shortcuts-group="false" :show-change-alert="false" :model="scope.row" /></div> </el-popover> <span class="model-ER" v-popover="`${scope.row.alias}-ERPopover`"> <el-icon name="el-ksd-icon-table_er_diagram_22" class="ksd-fs-22" type="mult"></el-icon> </span> <div class="fact-table" v-if="scope.row.fact_table.split('.').length === 2"><span v-custom-tooltip="{text: scope.row.fact_table.split('.')[1], w: 0, tableClassName: 'model_list_table'}">{{scope.row.fact_table.split('.')[1]}}</span></div> </template> </template> </el-table-column> <el-table-column prop="model_type" show-overflow-tooltip width="150px" v-if="$store.state.config.platform !== 'iframe'" :label="$t('modelType')"> <template slot-scope="scope"> <span>{{$t(scope.row.model_type)}}</span> </template> </el-table-column> <el-table-column width="100" prop="usage" align="right" sortable="custom" show-overflow-tooltip :info-tooltip="$t('usageTip', {mode: $t('model')})" :label="$t('usage')"> </el-table-column> <el-table-column width="120" prop="input_records_count" :label="$t('rowCount')"> <template slot-scope="scope"> <div>{{sliceNumber(scope.row.input_records_count)}}</div> <div class="update-time ksd-fs-12"> <span v-if="!!scope.row.last_build_time" v-custom-tooltip="{text: `${$t('lastBuildTime')}${transToServerGmtTime(scope.row.last_build_time)}`, content: transToServerGmtTime(scope.row.last_build_time), w: 0, tableClassName: 'model_list_table'}">{{transToServerGmtTime(scope.row.last_build_time)}}</span> <span v-else>-</span> </div> </template> </el-table-column> <el-table-column align="right" prop="expansionrate" show-overflow-tooltip width="150" :info-tooltip="$t('expansionRateTip')" :label="$t('expansionRate')" > <template slot-scope="scope"> <span v-if="scope.row.storage < 1073741824"> <el-tooltip placement="top" :content="$t('expansionTip')"><span>-</span></el-tooltip> </span> <template v-else> <span v-if="scope.row.expansion_rate !== '-1'">{{scope.row.expansion_rate}}%</span> <el-tooltip v-else class="item" effect="dark" :content="$t('tentativeTips')" placement="top"> <span class="is-disabled">{{$t('tentative')}}</span> </el-tooltip> </template> </template> </el-table-column> <el-table-column align="right" width="120" prop="total_indexes" show-overflow-tooltip :label="$t('aggIndexCount')"> <template slot-scope="scope"> <span>{{sliceNumber('streaming_indexes' in scope.row ? scope.row.total_indexes + scope.row.streaming_indexes : scope.row.total_indexes) || 0}}</span> </template> </el-table-column> </el-table> <!-- 分页 --> <kylin-pager class="ksd-center ksd-mtb-10" ref="pager" :perPageSize="filterArgs.page_size" :refTag="pageRefTags.modelListPager" :curPage="filterArgs.page_offset+1" :totalSize="modelsPagerRenderData.totalSize" v-on:handleCurrentChange='pageCurrentChange'></kylin-pager> </div> <!-- 模型构建 --> <ModelBuildModal @isWillAddIndex="willAddIndex" ref="modelBuildComp"/> <!-- 模型检查 --> <ModelCheckDataModal/> <!-- 数据分区设置 --> <ModelPartition/> <!-- 模型重命名 --> <ModelRenameModal/> <!-- 模型克隆 --> <ModelCloneModal/> <!-- 模型添加 --> <ModelAddModal/> <!-- 推荐模型 --> <UploadSqlModel v-on:reloadModelList="loadModelsList"/> <!-- 选择去构建的segment --> <ConfirmSegment v-on:reloadModelAndSegment="reloadModelAndSegment"/> </div> </template> <script> import Vue from 'vue' import { Component, Watch } from 'vue-property-decorator' import { mapActions, mapState, mapGetters, mapMutations } from 'vuex' import dayjs from 'dayjs' import { NamedRegex, pageRefTags, pageCount } from '../../../../config' import { ModelStatusTagType } from '../../../../config/model.js' import locales from './locales' import { handleError, kylinMessage, jumpToJobs } from '../../../../util/business' import { handleSuccessAsync, sliceNumber, transToServerGmtTime } from '../../../../util' import TableIndex from '../TableIndex/index.vue' import ModelSegment from './ModelSegment/index.vue' import SegmentTabs from './ModelSegment/SegmentTabs.vue' import TableIndexView from './TableIndexView/index.vue' import ModelRenameModal from './ModelRenameModal/rename.vue' import ModelCloneModal from './ModelCloneModal/clone.vue' import ModelAddModal from './ModelAddModal/addmodel.vue' import ModelBuildModal from './ModelBuildModal/build.vue' import ModelCheckDataModal from './ModelCheckData/checkdata.vue' import ConfirmSegment from './ConfirmSegment/ConfirmSegment.vue' import ModelPartition from './ModelPartition/index.vue' import ModelJson from './ModelJson/modelJson.vue' import ModelSql from './ModelSql/ModelSql.vue' import UploadSqlModel from '../../../common/UploadSql/UploadSql.vue' import { mockSQL } from './mock' import DropdownFilter from '../../../common/DropdownFilter/DropdownFilter.vue' import ModelOverview from './ModelOverview/ModelOverview.vue' import ModelActions from './ModelActions/modelActions' import ModelERDiagram from '../../../common/ModelERDiagram/ModelERDiagram' import ModelTitleDescription from './Components/ModelTitleDescription' function getDefaultFilters (that) { return { page_offset: 0, page_size: +localStorage.getItem(that.pageRefTags.modelListPager) || pageCount, exact: false, model_name: '', sort_by: 'last_modify', reverse: true, status: [], model_types: [], model_alias_or_owner: '', last_modify: [], owner: '', model_attributes: [], lite: true } } @Component({ beforeRouteEnter (to, from, next) { next(vm => { // 从编辑页面过来,要默认选中在某个tab上,从这里取 if (to.params.expandTab) { vm.currentEditModel = from.params.modelName vm.expandTab = to.params.expandTab // vm.showFull = true } if (to.params.modelAlias) { // vm.currentEditModel = to.params.modelAlias vm.filterArgs.model_alias_or_owner = to.params.modelAlias vm.filterArgs.exact = true } // 从ER图跳转回来的,模糊搜索模型 - KE if (to.query.modelListFilters) { vm.filterArgs = {...vm.filterArgs, ...JSON.parse(to.query.modelListFilters)} } if (to.query.model_alias) { vm.currentEditModel = to.query.model_alias vm.filterArgs.model_alias_or_owner = to.query.model_alias vm.filterArgs.exact = true } // onSortChange 中project有值时会 loadmodellist, 达到初始化数据的目的 vm.filterArgs.project = vm.currentSelectedProject const prop = 'gmtTime' const order = 'descending' vm.onSortChange({ prop, order }) }) }, computed: { ...mapGetters([ 'currentSelectedProject', 'modelsPagerRenderData', 'briefMenuGet', 'datasourceActions', 'modelActions', 'metadataActions', 'isOnlyQueryNode' ]), ...mapState({ currentUser: state => state.user.currentUser, platform: state => state.config.platform, filterModelNameByKC: state => state.model.filterModelNameByKC, streamingEnabled: state => state.system.streamingEnabled }) }, inject: [ 'isAdvancedOperatorUser' ], methods: { ...mapActions({ loadModels: 'LOAD_MODEL_LIST', checkModelName: 'CHECK_MODELNAME', updataModel: 'UPDATE_MODEL', getModelJson: 'GET_MODEL_JSON', fetchSegments: 'FETCH_SEGMENTS', downloadModelsMetadata: 'DOWNLOAD_MODELS_METADATA', downloadModelsMetadataBlob: 'DOWNLOAD_MODELS_METADATA_BLOB', getFavoriteRules: 'GET_FAVORITE_RULES', autoFixSegmentHoles: 'AUTO_FIX_SEGMENT_HOLES' }), ...mapActions('DetailDialogModal', { callGlobalDetailDialog: 'CALL_MODAL' }), ...mapActions('ModelAddModal', { callAddModelDialog: 'CALL_MODAL' }), ...mapActions('ModelBuildModal', { callModelBuildDialog: 'CALL_MODAL' }), ...mapActions('ConfirmSegment', { callConfirmSegmentModal: 'CALL_MODAL' }), ...mapActions('ModelRecommendModal', { callModelRecommendDialog: 'CALL_MODAL' }), ...mapActions('UploadSqlModel', { showUploadSqlDialog: 'CALL_MODAL' }), ...mapActions('ModelsImportModal', { callModelsImportModal: 'CALL_MODAL' }), ...mapActions('ModelsExportModal', { callModelsExportModal: 'CALL_MODAL' }), ...mapMutations({ updateFilterModelByCloud: 'UPDATE_FILTER_MODEL_NAME_CLOUD' }) }, components: { ModelBuildModal, TableIndex, ModelSegment, SegmentTabs, TableIndexView, ModelRenameModal, ModelCloneModal, ModelAddModal, ModelCheckDataModal, ConfirmSegment, ModelPartition, ModelJson, ModelSql, UploadSqlModel, DropdownFilter, ModelOverview, ModelActions, ModelERDiagram, ModelTitleDescription }, locales, provide () { return { getFavoriteRules: this.getFavoriteRulesContext } } }) export default class ModelList extends Vue { pageRefTags = pageRefTags mockSQL = mockSQL sliceNumber = sliceNumber transToServerGmtTime = transToServerGmtTime filterArgs = getDefaultFilters(this) statusList = ['ONLINE', 'OFFLINE', 'BROKEN', 'WARNING'] modelTypeList = ['HYBRID', 'STREAMING', 'BATCH'] currentEditModel = null showFull = false showSearchResult = false searchLoading = false isShowFilters = true modelArray = [] expandedRows = [] filterTags = [] prevExpendContent = [] buildVisible = {} changeOwnerVisible = false expandTab = '' isModelListOpen = false isShow = false loadingModels = false debouce = null @Watch('modelsPagerRenderData') onModelChange (modelsPagerRenderData) { this.modelArray = [] modelsPagerRenderData.list.forEach(item => { this.$set(item, 'showModelDetail', false) this.modelArray.push({ ...item, tabTypes: this.currentEditModel === item.alias ? this.expandTab : 'overview', showER: false }) }) } // 获取模型 E-R 图数据 getER (row) { return this.dataGenerator.generateModel(row) } showGenerateModelDialog () { this.showUploadSqlDialog({ isGenerateModel: true }) } needShowBuildTips (uuid) { this.buildVisible[uuid] = !localStorage.getItem('hideBuildTips') } reloadModelAndSegment (alias) { this.loadModelsList() this.refreshSegment(alias) } async autoFix (...args) { try { const [modelName, modleId, segmentHoles] = args 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: modelName}), 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, 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: modleId, segment_holes: selectSegmentHoles}) 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.submitSuccess')}</span> <a href="javascript:void(0)" onClick={() => jumpToJobs()}>{this.$t('kylinLang.common.toJoblist')}</a> </div> ) }) this.loadModelsList() } catch (e) { handleError(e) } } }) } catch (e) { e !== 'cancel' && handleError(e) } } openComplementSegment (model, isModelMetadataChanged) { let title let subTitle let submitText let refrashWarningSegment if (isModelMetadataChanged) { title = this.$t('kylinLang.common.seeDetail') subTitle = '' refrashWarningSegment = true submitText = this.$t('kylinLang.common.refresh') } else { title = this.$t('buildIndex') subTitle = this.model.model_type === 'HYBRID' ? this.$t('hybridModelBuildTitle') : this.$t('batchBuildSubTitle') submitText = this.$t('buildIndex') } this.callConfirmSegmentModal({ title: title, subTitle: subTitle, refrashWarningSegment: refrashWarningSegment, indexes: [], submitText: submitText, model: model }) } setRowClass (res) { const {row, rowIndex} = res return 'visible' in row && !row.visible ? `model_list_row_${rowIndex} no-authority-model` : `model_list_row_${rowIndex}` } get emptyText () { return this.filterArgs.model_alias_or_owner ? this.$t('kylinLang.common.noResults') : this.$t('kylinLang.common.noData') } get modelTableTitle () { return this.$t('kylinLang.model.modelNameGrid') } get selectedStatus () { const { filterArgs } = this return filterArgs.status } get selectedRange () { const { filterArgs } = this if (filterArgs.last_modify && filterArgs.last_modify.length !== 0) { const [startTime, endTime] = filterArgs.last_modify const startDate = dayjs(startTime).format('YYYY-MM-DD HH:mm:ss') const endDate = dayjs(endTime).format('YYYY-MM-DD HH:mm:ss') return `${startDate} - ${endDate}` } return '' } get selectedModelAttributes () { const { filterArgs } = this return filterArgs.model_attributes } get isResetFilterDisabled () { return !this.filterArgs.last_modify.length && !this.filterArgs.status.length && !this.filterArgs.model_attributes.length } handleFilterInput (value) { this.filterArgs.model_alias_or_owner = value } handleResetFilters () { const defaultFilters = getDefaultFilters(this) Object.entries(defaultFilters).map(([key, value]) => { if (key === 'model_alias_or_owner') return this.filterArgs[key] = value }) this.pageCurrentChange(0, this.filterArgs.page_size) } handleToggleFilters () { this.isShowFilters = !this.isShowFilters } getModelStatusTagType = ModelStatusTagType renderFullExpandClass (row) { return (row.showModelDetail || this.currentEditModel === row.alias) ? 'full-cell-content' : '' } expandRow (row, expandedRows) { this.expandedRows = expandedRows && expandedRows.map((m) => { return Object.prototype.toString.call(m) === '[object Object]' ? m.alias : m }) || [] this.currentEditModel = null } renderRowKey (row) { return row.alias } renderColumnClass ({row, column, rowIndex, columnIndex}) { if (columnIndex === 0) { return 'model-alias-item' } if (column.label && column.label === this.$t('kylinLang.common.fact')) { return 'fact-table-title' } } checkName (rule, value, callback) { if (!NamedRegex.test(value)) { callback(new Error(this.$t('kylinLang.common.nameFormatValidTip'))) } else { callback() } } _showFullDataLoadConfirm (storage, modelName) { const storageSize = Vue.filter('dataSize')(storage) const contentVal = { modelName, storageSize } const confirmTitle = this.$t('fullLoadDataTitle') const confirmMessage1 = this.$t('fullLoadDataContent1', contentVal) const confirmMessage2 = this.$t('fullLoadDataContent2', contentVal) const confirmMessage3 = this.$t('fullLoadDataContent3', contentVal) const confirmMessage = _render(this.$createElement) const confirmButtonText = this.$t('kylinLang.common.ok') const cancelButtonText = this.$t('kylinLang.common.cancel') const type = 'warning' return this.$confirm(confirmMessage, confirmTitle, { confirmButtonText, cancelButtonText, type }) function _render (h) { return ( <div> <p class="break-all">{confirmMessage1}</p> <p>{confirmMessage2}</p> <p>{confirmMessage3}</p> </div> ) } } async refreshSegment (alias) { this.$refs['segmentComp' + alias] && await this.$refs['segmentComp' + alias].$emit('refresh') this.prevExpendContent = this.modelArray.filter(item => this.expandedRows.includes(item.alias)) this.$nextTick(() => { this.setModelExpand() }) } // 还原模型列表展开状态 async setModelExpand () { if (!this.$refs.modelListTable) return let obj = {} this.prevExpendContent.forEach(item => { obj[item.alias] = item }) this.modelArray.forEach(it => { (it.alias in obj) && (it.tabTypes = obj[it.alias].tabTypes) }) this.$refs.modelListTable.store.states.expandRows = [] this.expandedRows.length && this.expandedRows.forEach(item => { this.$refs.modelListTable.toggleRowExpansion(item) }) } downloadResouceData (project, form) { const params = {} for (const [key, value] of Object.entries(form)) { if (value instanceof Array) { value.forEach((item, index) => { params[`${key}[${index}]`] = item }) } else if (typeof value === 'object') { params[key] = JSON.stringify(value) } else { params[key] = value } } try { this.downloadModelsMetadataBlob({project, params}).then(res => { let str = res && res.headers.map['content-disposition'][0] let fileName1 = str.split('filename=')[1] let fileName = fileName1.includes('"') ? JSON.parse(fileName1) : fileName1 if (res && res.body) { let data = res.body const blob = new Blob([data], {type: 'application/json;charset=utf-8'}) if (window.navigator.msSaveOrOpenBlob) { navigator.msSaveBlob(data, fileName) } else { let link = document.createElement('a') link.href = window.URL.createObjectURL(blob) link.download = fileName link.click() window.URL.revokeObjectURL(link.href) } } this.$message.success(this.$t('exportMetadataSuccess')) }) } catch (e) { this.$message.error(this.$t('exportMetadataFailed')) } } handleSaveModel (modelDesc) { // 如果未选择partition 把partition desc 设置为null if (!(modelDesc && modelDesc.partition_desc && modelDesc.partition_desc.partition_date_column)) { modelDesc.partition_desc = null } this.updataModel(modelDesc).then(() => { kylinMessage(this.$t('kylinLang.common.saveSuccess')) this.loadModelsList() }, (res) => { handleError(res) }) } async handleImportModels () { const project = this.currentSelectedProject const isSubmit = await this.callModelsImportModal({ project }) if (isSubmit) { this.pageCurrentChange(0, this.filterArgs.page_size) } } async handleExportMetadatas () { const project = this.currentSelectedProject const type = 'all' await this.callModelsExportModal({ project, type }) } onSortChange ({ prop, order }) { this.filterArgs.sort_by = prop if (prop === 'gmtTime') { this.filterArgs.sort_by = 'last_modify' } this.filterArgs.reverse = !(order === 'ascending') if (this.filterArgs.project) { this.pageCurrentChange(0, this.filterArgs.page_size) } } // 全屏查看模型附属信息 toggleShowFull (index, row) { var scrollBoxDom = document.getElementById('scrollContent') if (!this.showFull && scrollBoxDom) { // 展开时记录下展开时候的scrollbar 的top距离,搜索的时候复原该位置 row.hisScrollTop = scrollBoxDom.scrollTop } this.$nextTick(() => { this.$set(row, 'showModelDetail', !this.showFull) this.showFull = !this.showFull this.$nextTick(() => { if (scrollBoxDom) { if (this.showFull) { scrollBoxDom.scrollTop = 0 } else { scrollBoxDom.scrollTop = row.hisScrollTop } this.currentEditModel = null } }) }) } // 加载模型列表 loadModelsList () { if (!this.currentSelectedProject) return this.prevExpendContent = this.modelArray.filter(item => this.expandedRows.includes(item.alias)) this.loadingModels = true this.$el.click() return this.loadModels(this.filterArgs).then(() => { if (this.filterArgs.model_alias_or_owner || this.modelsPagerRenderData.list.length) { this.showSearchResult = true } else { this.showSearchResult = false } this.loadingModels = false this.$nextTick(() => { this.expandedRows = this.currentEditModel ? [this.currentEditModel] : this.expandedRows this.setModelExpand() }) }).catch((res) => { this.loadingModels = false handleError(res) }) } // 分页 pageCurrentChange (size, count) { this.filterArgs.page_offset = size this.filterArgs.page_size = count this.loadModelsList() } // 搜索模型 searchModels () { if (this.filterArgs.exact) { this.filterArgs.exact = false } this.filterArgs.page_offset = 0 this.searchLoading = true this.updateFilterModelByCloud('') this.loadModelsList().then(() => { this.searchLoading = false }).finally((res) => { this.searchLoading = false }) } showAddModelDialog () { if (!this.modelArray.length && !localStorage.getItem('isFirstAddModel')) { localStorage.setItem('isFirstAddModel', 'true') } this.callAddModelDialog() } // 查询状态过滤回调函数 filterContent (val, type) { const maps = { status: 'status', model_types: 'model_types' } this.filterTags = this.filterTags.filter((item, index) => item.key !== type || item.key === type && val.includes(item.label)) const list = this.filterTags.filter(it => it.key === type).map(it => it.label) val.length && val.forEach(item => { if (!list.includes(item)) { this.filterTags.push({label: item, source: maps[type], key: type}) } }) this.filterArgs[type] = val clearTimeout(this.debouce) this.debouce = setTimeout(() => { this.pageCurrentChange(0, this.filterArgs.page_size) }, 300) } // 删除单个筛选条件 handleClose (tag) { const index = this.filterArgs[tag.key].indexOf(tag.label) index > -1 && this.filterArgs[tag.key].splice(index, 1) this.filterTags = this.filterTags.filter(item => item.key !== tag.key || item.key === tag.key && tag.label !== item.label) this.pageCurrentChange(0, this.filterArgs.page_size) } // 清除所有筛选条件 clearAllTags () { this.filterArgs.status.splice(0, this.filterArgs.status.length) this.filterTags = [] this.pageCurrentChange(0, this.filterArgs.page_size) } renderStatusLabel (h, option) { const { value } = option return [ <i class={['filter-bar filter-status', value]} />, <span>{value}</span> ] } renderModelTypeLabel (h, option) { const { value } = option return [ <span>{this.$t(value)}</span> ] } get modelAttributesOptions () { let options = [] if (this.streamingEnabled === 'false') { // 暂不支持流数据融合数据模型 或者没有开启流模型配置 options = [ { renderLabel: this.renderModelTypeLabel, value: 'BATCH' } ] } else { options = [ { renderLabel: this.renderModelTypeLabel, value: 'HYBRID' }, { renderLabel: this.renderModelTypeLabel, value: 'STREAMING' }, { renderLabel: this.renderModelTypeLabel, value: 'BATCH' } ] } return options } // 模型展开自动滚动到可视区域 scrollViewArea (index) { const scrollDom = this.$el.querySelector(`.model_list_row_${index}`) scrollDom.nextSibling.querySelector('.aggregate-view').scrollIntoView({block: 'center', inline: 'nearest', behavior: 'smooth'}) } async getFavoriteRulesContext () { try { const results = await this.getFavoriteRules({ project: this.currentSelectedProject }) const res = await handleSuccessAsync(results) return res } catch (err) { handleError(err) return {} } } async willAddIndex (alias) { this.$refs['segmentComp' + alias] && await this.$refs['segmentComp' + alias].$emit('willAddIndex') } modelRowClickEvent (row, e) { if (row.status === 'BROKEN' || ('visible' in row && !row.visible)) return this.$router.push({ name: 'ModelDetails', params: {modelName: row.alias}, query: {modelListFilters: JSON.stringify(this.filterArgs)} }) } // 展示 E-R 图 afterPoppoverEnter (e, row) { this.$nextTick(() => { this.$set(row, 'showER', true) this.$nextTick(() => { this.$refs.erDiagram && this.$refs.erDiagram.exchangePosition() }) }) } // 跳转至指定模型优化建议界面 jumpToRecommendation (model) { this.$router.push({ name: 'ModelDetails', params: { modelName: model.alias, jump: 'recommendation' }, query: { modelListFilters: JSON.stringify(this.filterArgs) } }) } handleAnimateChanged (event) { const { className } = event.target const isValidClass = className.indexOf('mode-list') !== -1 || className.indexOf('el-loading-mask') !== -1 || className.indexOf('el-button') !== -1 if (isValidClass) { const $table = this.$refs['modelListTable'] if ($table) $table.verifyPosition() } } mounted () { const $contentPage = document.querySelector('.main-content .mode-list') if ($contentPage) { $contentPage.addEventListener('transitionend', this.handleAnimateChanged) } } beforeDestroy () { const $contentPage = document.querySelector('.main-content .mode-list') if ($contentPage) { $contentPage.removeEventListener('transitionend', this.handleAnimateChanged) } } } </script> <style lang="less"> @import '../../../../assets/styles/variables.less'; .mode-list{ position:relative; margin-left: 24px; margin-right: 24px; .model-list-contain { position: relative; } .ts-storage { font-size: 12px; color: @text-disabled-color; } .specialDropdown{ min-width:96px; .el-dropdown-menu__item { white-space: nowrap; } } .dropdown-filter + .dropdown-filter { margin-left: 5px; } .broken-column { .cell { display: none; } } .full-model-slide-fade-enter-active { transition: all .3s ease; } .full-model-slide-fade-leave-active { transition: all .3s cubic-bezier(1.0, 0.5, 0.8, 1.0); } .full-model-slide-fade-enter, .full-model-slide-fade-leave-to { transform: translateY(10px); opacity: 0; } .row-action { position: absolute; right:0; text-align: right; z-index: 2; cursor: pointer; color: @text-normal-color; &:hover { color: @base-color; .tip-text { color: @base-color; } } .tip-text { top:10px; color: @text-normal-color; } } .notice-box { position:relative; .el-alert{ background-color:@base-color-9; a { text-decoration: underline; color:@base-color-1; } } .tip-toggle-btnbox { position:absolute; top:4px; right:10px; } } .model_list_table { user-select: none; .el-table__header th { vertical-align: top; } .el-table__body tr { cursor: pointer; } .el-table__body td { vertical-align: top; } .custom-tooltip-layout { vertical-align: middle; width: calc(~'100% - 5px'); } .model-ER { cursor: pointer; } .rec-btn { color: @ke-color-primary; button-text { min-width: initial; } } .recommendation-layout { display: inline-block; padding: 5px 10px; border: 1px solid @ke-border-secondary; border-radius: 6px; color: @ke-color-primary; cursor: pointer; } .build-disabled > .el-ksd-icon-build_index_22 { color: @color-text-disabled; &:hover { color: @color-text-disabled; } } .clickable-btn { color: @base-color; cursor: pointer; } span.is-disabled { color: @text-disabled-color; } .el-table__expanded-cell { background-color: @background-color-base-1; padding:0; &:hover { background-color: @background-color-base-1 !important; } .full-cell-content { position: relative; } .full-model-box { vertical-align:middle; font-size: 16px; margin-left:10px; z-index: 10; } .model-detail-tabs { &.el-tabs--card>.el-tabs__header .el-tabs__item.is-active{ border-bottom-color: #fbfbfb; } > .el-tabs__header { margin-bottom: 0px; z-index: 1; .el-tabs__item { height: 40px; line-height: 40px; } .el-tabs__item.is-active{ border-bottom-color: #fbfbfb; } &> .el-tabs__nav-wrap { box-shadow:0px 2px 4px 0px rgba(229,229,229,1); background-color: @fff; } } } } .el-table__row.no-authority-model { background-color: #f5f5f5; color: @text-disabled-color; // pointer-events: none; } .el-icon-ksd-filter { position: relative; font-size: 17px; top: 2px; left: 5px; &:hover, &.filter-open { color: @base-color; } } .cell.highlight { .el-icon-ksd-filter { color: @base-color; } } .ky-hover-icon { .cell { .tip_box { margin-left: 10px; &:first-child { margin-left: 0; } } } } .el-icon-ksd-lock { color: @text-title-color; } .model-alias-title { max-width: calc(~'100% - 30px'); } .model-alias-item { .action-items { opacity: 0; position: absolute; right: 10px; top: 6px; } &:hover { .model-alias-title { max-width: calc(~'100% - 120px'); } .action-items { opacity: 1; } } } .fact-table-title { .fact-table { width: calc(~'100% - 35px'); margin-left: 8px; display: inline-block; } .cell > span { width: 100%; display: inline-block; } } .update-time { width: 100%; color: @text-disabled-color; overflow: hidden; text-overflow: ellipsis; span { font-size: 12px; } } } .row-action { right:20px; top: 4px; } &.full-cell { margin: 0 20px; position: relative; .segment-settings { display: block; } .segment-actions .left { display: block; } .model_list_table { position: static !important; border: none; td { position: static !important; } .el-table__body-wrapper { position: static !important; .el-table__expanded-cell { padding: 0; .full-cell-content { z-index: 9; position: absolute; padding-top: 10px; background: @fff; top: 0px; height: 100vh; width: calc(~'100% + 40px'); padding-right: 20px; padding-left: 20px; margin-left: -20px; border-top: 1px solid #CFD8DC; &.hidden-cell { display: none; } .full-model-box { top: 20px; right: 20px; } } } } } } .el-tabs__nav { margin-left: 0; } .el-tabs__content { overflow: initial; } .actions { line-height: 22px; // border-right: 1px solid @ke-border-divider-color; margin: 6px 8px 0 0; padding-right: 4px; height: 22px; display: inline-block; .reset-filters-btn.is-disabled { i { cursor: not-allowed; } } } .table-filters { margin-bottom: 8px; >.dropdown-filter { margin-left: -8px; } } .last-modified { font-size: 12px; line-height: 18px; float: left; margin-right: 15px; i { color: #989898; cursor: default; } } .recommend { font-size: 12px; line-height: 18px; float: left; color: @color-primary; i { color: @text-disabled-color; cursor: default; } } .recommend-count { height: 18px; border-radius: 4px; background-color: @base-color-4; color: @fff; padding: 1px 5px; line-height: 16px; font-weight: bold; margin-left: 2px; cursor: pointer; b { position: relative; transform: scale(0.833333); display: inline-block; } } .streaming { float: left; font-size: 12px; line-height: 18px; } .alias .filter-status { float: left; position: relative; top: 6px; } .text-container { overflow: hidden; text-overflow: ellipsis; } .text-sample { // white-space: nowrap; 在safari浏览器上有问题 display: -webkit-box; -webkit-line-clamp: 1; /*! autoprefixer: off */ -webkit-box-orient: vertical; /* autoprefixer: on */ white-space: nowrap\0; } } .no-acl-model { .dialog-detail { .dialog-detail-scroll { max-height: 200px; } } } .model_list_table{ .model-alias-label { cursor: pointer; .alias { height: 20px; margin-top: 0; cursor: pointer; .filter-status { cursor: pointer; } } .last-modified-tooltip { cursor: pointer; } } } .filter-button { margin-left: 5px; vertical-align: bottom; .el-ksd-icon-arrow_up_22 { transform: rotate(180deg); } .el-ksd-icon-arrow_up_22.reverse { transform: rotate(0); } } .filter-status { border-radius: 100%; width: 10px; height: 10px; display: inline-block; margin-right: 10px; &.ONLINE { background-color: @color-success; } &.OFFLINE { background-color: @ke-color-info-secondary; } &.BROKEN { background-color: @color-danger; } &.WARNING { background-color: @color-warning; } } .filter-bar { &.filter-status { border-radius: 100%; width: 10px; height: 10px; display: inline-block; margin-right: 10px; &.ONLINE { background-color: @color-success; } &.OFFLINE { background-color: @ke-color-info-secondary; } &.BROKEN { background-color: @color-danger; } &.WARNING { background-color: @color-warning; } } } .recommend-tooltip { min-width: unset; transform: translate(-5px, 5px); .popper__arrow { left: 5px !important; } .recommend-link { color: @color-primary; cursor: pointer; } } .model-actions-dropdown { text-align: left; min-width: 95px; } .er-popover-layout { width: 400px; height: 300px; position: relative; background-color: @ke-background-color-secondary; .model-ER-layout { width: 100%; height: 100%; position: relative; overflow: hidden; } } .el-tooltip__popper { .popper__arrow { &::after { content: none\0; } } } </style>