From 0d82d52899f02337166aabd7627eb18e31c8ca9f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Piggy=20Park=20=28=EB=B0=95=EC=9A=A9=ED=83=9C=29?= Date: Wed, 12 Feb 2025 09:51:25 +0900 Subject: [PATCH 1/2] fix(cost-analysis): apply project/service-account link to cost data table (#5634) Signed-off-by: samuel.park --- .../components/CostAnalysisDataTable.vue | 39 ++++++++++++++++++- 1 file changed, 37 insertions(+), 2 deletions(-) diff --git a/apps/web/src/services/cost-explorer/components/CostAnalysisDataTable.vue b/apps/web/src/services/cost-explorer/components/CostAnalysisDataTable.vue index faea12e669..a374f2c120 100644 --- a/apps/web/src/services/cost-explorer/components/CostAnalysisDataTable.vue +++ b/apps/web/src/services/cost-explorer/components/CostAnalysisDataTable.vue @@ -2,6 +2,7 @@ import { computed, reactive, watch, } from 'vue'; +import { useRouter } from 'vue-router/composables'; import dayjs from 'dayjs'; import { @@ -43,7 +44,9 @@ import type { ExcelDataField } from '@/lib/helper/file-download-helper/type'; import { usageUnitFormatter } from '@/lib/helper/usage-formatter'; import ErrorHandler from '@/common/composables/error/errorHandler'; +import { useProperRouteLocation } from '@/common/composables/proper-route-location'; +import { ASSET_INVENTORY_ROUTE_V1 } from '@/services/asset-inventory-v1/routes/route-constant'; import { GRANULARITY, GROUP_BY, @@ -61,6 +64,7 @@ import type { Period, DisplayDataType, } from '@/services/cost-explorer/types/cost-explorer-query-type'; +import { PROJECT_ROUTE } from '@/services/project/routes/route-constant'; type CostAnalyzeRawData = { @@ -78,6 +82,9 @@ const allReferenceStore = useAllReferenceStore(); const costAnalysisPageStore = useCostAnalysisPageStore(); const costAnalysisPageGetters = costAnalysisPageStore.getters; const costAnalysisPageState = costAnalysisPageStore.state; +const router = useRouter(); +const { getProperRouteLocation } = useProperRouteLocation(); + const getValueSumKey = (dataType: string) => { switch (dataType) { @@ -392,6 +399,28 @@ const listCostAnalysisExcelData = async (): Promise => { }; /* event */ +const handleClickRowData = (fieldName: string, value: string) => { + if (!fieldName || !value) return; + + let _routeName: string; + let _params = {}; + + if (fieldName === GROUP_BY.PROJECT) { + _routeName = PROJECT_ROUTE.DETAIL._NAME; + _params = { id: value }; + } + if (fieldName === GROUP_BY.SERVICE_ACCOUNT) { + _routeName = ASSET_INVENTORY_ROUTE_V1.SERVICE_ACCOUNT.DETAIL._NAME; + _params = { serviceAccountId: value }; + } + + if (!_routeName) return; + + window.open(router.resolve(getProperRouteLocation({ + name: _routeName, + params: _params, + })).href, '_blank'); +}; const handleChange = async (options: any = {}) => { setApiQueryWithToolboxOptions(analyzeApiQueryHelper, options, { queryTags: true, @@ -533,7 +562,10 @@ watch( : value }} - + {{ storeState.projects[value] ? storeState.projects[value].label @@ -552,7 +584,10 @@ watch( storeState.regions[value] ? storeState.regions[value].name : value }} - + {{ storeState.serviceAccounts[value] ? storeState.serviceAccounts[value].name From addd79aa4ca3c76b298ca15640badbcdadda4916 Mon Sep 17 00:00:00 2001 From: Yongtae Park Date: Wed, 12 Feb 2025 18:39:56 +0900 Subject: [PATCH 2/2] feat(group-by-tags): create group by tags form Signed-off-by: samuel.park --- .../WidgetFormDataTableCardAddContents.vue | 45 ++++++++- .../WidgetFormDataTableCardAddForm.vue | 97 ++++++++++++++++++- .../widgets/_constants/data-table-constant.ts | 11 +++ .../modules/widgets/types/widget-model.ts | 9 +- .../src/component-util/query-search/index.ts | 2 +- 5 files changed, 158 insertions(+), 6 deletions(-) diff --git a/apps/web/src/common/modules/widgets/_components/WidgetFormDataTableCardAddContents.vue b/apps/web/src/common/modules/widgets/_components/WidgetFormDataTableCardAddContents.vue index 79e9125c47..ac93d403a0 100644 --- a/apps/web/src/common/modules/widgets/_components/WidgetFormDataTableCardAddContents.vue +++ b/apps/web/src/common/modules/widgets/_components/WidgetFormDataTableCardAddContents.vue @@ -31,7 +31,7 @@ import WidgetFormDataTableCardSourceForm import { DATA_SOURCE_DOMAIN, DATA_TABLE_OPERATOR, - DATA_TABLE_TYPE, + DATA_TABLE_TYPE, GROUP_BY_INFO_ITEMS_FOR_TAGS, } from '@/common/modules/widgets/_constants/data-table-constant'; import { useWidgetGenerateStore } from '@/common/modules/widgets/_store/widget-generate-store'; import type { DataTableAlertModalMode } from '@/common/modules/widgets/types/widget-data-table-type'; @@ -40,6 +40,8 @@ import type { DataTableQueryFilter, TimeDiff, } from '@/common/modules/widgets/types/widget-model'; +import { GROUP_BY } from '@/services/cost-explorer/constants/cost-explorer-constant'; + interface Props { selected: boolean; item: PublicDataTableModel|PrivateDataTableModel; @@ -73,7 +75,11 @@ const state = reactive({ selectedSourceEndItem: props.item.source_type === DATA_SOURCE_DOMAIN.COST ? props.item.options[DATA_SOURCE_DOMAIN.COST]?.data_key : props.item.options[DATA_SOURCE_DOMAIN.ASSET]?.metric_id, - selectedGroupByItems: [] as { name: string; label: string; }[], + selectedGroupByItems: [] as { name: string; label: string; tags?: [] }[], + selectedGroupByTagsMap: { + [GROUP_BY.PROJECT]: [], + [GROUP_BY.REGION]: [], + } as Record, filter: {} as Record, dataFieldName: '', dataUnit: '', @@ -107,13 +113,15 @@ const state = reactive({ optionsChanged: computed(() => { const sourceKeyChanged = state.selectedSourceEndItem !== originDataState.sourceKey; const groupByChanged = !isEqual(state.selectedGroupByItems, originDataState.groupBy); + const groupByTagsChanged = !isEqual(state.selectedGroupByTagsMap, originDataState.groupByTagsMap); const filterChanged = !isEqual(state.filter, originDataState.filter); const dataTableNameChanged = state.dataFieldName !== originDataState.dataName; const dataUnitChanged = state.dataUnit !== originDataState.dataUnit; const timeDiffChanged = advancedOptionsState.selectedTimeDiff !== originDataState.timeDiff; const timeDiffDateChanged = advancedOptionsState.selectedTimeDiffDate !== originDataState.timeDiffDate; - return sourceKeyChanged || groupByChanged || filterChanged || dataTableNameChanged || dataUnitChanged + return sourceKeyChanged || groupByChanged || groupByTagsChanged || filterChanged + || dataTableNameChanged || dataUnitChanged || timeDiffChanged || timeDiffDateChanged; }), failStatus: false, @@ -139,7 +147,22 @@ const originDataState = reactive({ groupBy: computed(() => ((props.item.options as DataTableAddOptions).group_by ?? []).map((group) => ({ name: group.key, label: group.name, + tags: group.tags, }))), + groupByTagsMap: computed(() => { + const _groupByTagsMap = { + [GROUP_BY.PROJECT]: [], + [GROUP_BY.REGION]: [], + } as Record; + ((props.item.options as DataTableAddOptions).group_by ?? []).forEach((group) => { + const isGroupByTags = GROUP_BY_INFO_ITEMS_FOR_TAGS.some((tag) => tag.key === group.key); + if (isGroupByTags) { + const tagsMenu = group.tags?.map((tag) => ({ name: tag, label: tag })); + _groupByTagsMap[group.key as string] = tagsMenu || []; + } + }); + return _groupByTagsMap; + }), filter: computed>(() => { const _filter = {} as Record; ((props.item.options as DataTableAddOptions).filter ?? []).forEach((filter) => { @@ -201,10 +224,24 @@ const updateDataTable = async (): Promise => { const costGroupBy = state.selectedGroupByItems.map((group) => ({ key: group.name, name: group.label, + tags: group.tags, })); const metricLabelsInfo = storeState.metrics[state.metricId ?? '']?.data?.labels_info; const assetGroupBy = (metricLabelsInfo ?? []).filter((label) => state.selectedGroupByItems.map((group) => group.name).includes(label.key)); const groupBy = state.sourceType === DATA_SOURCE_DOMAIN.COST ? costGroupBy : assetGroupBy; + + GROUP_BY_INFO_ITEMS_FOR_TAGS.forEach((tag) => { + const groupByTags = groupBy.find((group) => group.key === tag.key); + if (groupByTags) { + groupBy.map((group) => { + if (tag.key === group.key) { + group.tags = state.selectedGroupByTagsMap[tag.key]?.map((item) => item.name) || []; + } + return group; + }); + } + }); + const refinedFilter = Object.values(state.filter as Record) .filter((filter) => { if (isArray(filter.v)) return filter?.v?.length; @@ -311,6 +348,7 @@ const setInitialDataTableForm = () => { // Initial Form Setting // Basic Options state.selectedGroupByItems = [...originDataState.groupBy]; + state.selectedGroupByTagsMap = { ...originDataState.groupByTagsMap }; state.filter = originDataState.filter; state.dataFieldName = originDataState.dataName; state.dataUnit = originDataState.dataUnit; @@ -379,6 +417,7 @@ defineExpose({ :source-type="state.sourceType" :source-items="state.selectableSourceItems" :selected-group-by-items.sync="state.selectedGroupByItems" + :selected-group-by-tags-map.sync="state.selectedGroupByTagsMap" :filter.sync="state.filter" :data-field-name.sync="state.dataFieldName" :data-unit.sync="state.dataUnit" diff --git a/apps/web/src/common/modules/widgets/_components/WidgetFormDataTableCardAddForm.vue b/apps/web/src/common/modules/widgets/_components/WidgetFormDataTableCardAddForm.vue index 3a32284605..ea0842a984 100644 --- a/apps/web/src/common/modules/widgets/_components/WidgetFormDataTableCardAddForm.vue +++ b/apps/web/src/common/modules/widgets/_components/WidgetFormDataTableCardAddForm.vue @@ -6,11 +6,15 @@ import type { TranslateResult } from 'vue-i18n'; import { cloneDeep, range } from 'lodash'; +import { + makeDistinctValueHandler, +} from '@cloudforet/core-lib/component-util/query-search'; import { PFieldGroup, PSelectDropdown, PTextInput, } from '@cloudforet/mirinae'; import type { MenuItem } from '@cloudforet/mirinae/src/controls/context-menu/type'; import type { SelectDropdownMenuItem } from '@cloudforet/mirinae/src/controls/dropdown/select-dropdown/type'; +import type { AutocompleteHandler } from '@cloudforet/mirinae/types/controls/dropdown/select-dropdown/type'; import { i18n } from '@/translations'; @@ -20,15 +24,23 @@ import type { CostDataSourceReferenceMap } from '@/store/reference/cost-data-sou import type { MetricReferenceMap } from '@/store/reference/metric-reference-store'; import { showErrorMessage } from '@/lib/helper/notice-alert-helper'; +import { + MANAGED_VARIABLE_MODELS, type ManagedVariableModelKey, +} from '@/lib/variable-models/managed-model-config/base-managed-model-config'; import { useCostDataSourceFilterMenuItems } from '@/common/composables/data-source/use-cost-data-source-filter-menu-items'; +import ErrorHandler from '@/common/composables/error/errorHandler'; import { useProxyValue } from '@/common/composables/proxy-state'; import WidgetFormDataTableCardFilters from '@/common/modules/widgets/_components/WidgetFormDataTableCardFilters.vue'; -import { DATA_SOURCE_DOMAIN } from '@/common/modules/widgets/_constants/data-table-constant'; +import { + DATA_SOURCE_DOMAIN, + GROUP_BY_INFO_ITEMS_FOR_TAGS, +} from '@/common/modules/widgets/_constants/data-table-constant'; import type { DataTableQueryFilter } from '@/common/modules/widgets/types/widget-model'; import { PROJECT_GROUP_LABEL_INFO } from '@/services/asset-inventory-v1/constants/asset-analysis-constant'; +const TAGS_DATA_KEY = 'tags'; interface Props { @@ -39,6 +51,7 @@ interface Props { sourceKey: string; sourceItems: SelectDropdownMenuItem[]; selectedGroupByItems: any[]; + selectedGroupByTagsMap: Record; filter: Record; dataFieldName: string; dataUnit: string; @@ -55,6 +68,7 @@ const MAX_GROUP_BY_COUNT = 5; const props = defineProps(); const emit = defineEmits<{(e: 'update:filter', value: Record): void; (e: 'update:selected-group-by-items', value: any[]): void; + (e: 'update:selected-group-by-tags-map', value: Record): void; (e: 'update:data-field-name', value: string): void; (e: 'update:data-unit', value: string): void; (e: 'update:selected-time-diff', value: string): void; @@ -78,6 +92,7 @@ const { allItems: costDataSourceMenuItems } = useCostDataSourceFilterMenuItems({ const state = reactive({ proxySelectedGroupByItems: useProxyValue('selectedGroupByItems', props, emit), + proxySelectedGroupByTagsMap: useProxyValue('selectedGroupByTagsMap', props, emit), proxyDataFieldName: useProxyValue('dataFieldName', props, emit), proxyDataUnit: useProxyValue('dataUnit', props, emit), proxyFilter: useProxyValue>('filter', props, emit), @@ -144,6 +159,32 @@ const groupByState = reactive({ }), }); +const groupByTagsState = reactive({ + tagsAreaVisible: computed(() => state.proxySelectedGroupByItems.some((d) => GROUP_BY_INFO_ITEMS_FOR_TAGS.some((item) => item.key === d.name))), + tagsAreaItems: computed(() => state.proxySelectedGroupByItems.filter((d) => GROUP_BY_INFO_ITEMS_FOR_TAGS.some((item) => item.key === d.name))), + loading: false, + valueHandlerMap: computed(() => { + const handlerMaps = {}; + state.proxySelectedGroupByItems.forEach(({ name }) => { + const groupByItemInfo = GROUP_BY_INFO_ITEMS_FOR_TAGS.find((d) => d.key === name); + if (groupByItemInfo) { + handlerMaps[groupByItemInfo.key] = getValueHandlerMap(groupByItemInfo.name); + } + }); + return handlerMaps; + }), + selectedMap: computed(() => { + const selectedMap = {}; + state.proxySelectedGroupByItems.forEach((item) => { + const groupByItemInfo = GROUP_BY_INFO_ITEMS_FOR_TAGS.find((d) => d.key === item.name); + if (groupByItemInfo) { + selectedMap[item.name] = item.tags || []; + } + }); + return selectedMap; + }), +}); + const assetFilterState = reactive({ refinedLabelKeys: computed(() => { const metricLabelsInfo = storeState.metrics[props.sourceId ?? '']?.data?.labels_info; @@ -197,6 +238,23 @@ const resetAllFilter = () => { state.proxyFilter = {}; }; +const getValueHandlerMap = (name: ManagedVariableModelKey): AutocompleteHandler => { + const resourceKey = MANAGED_VARIABLE_MODELS[name]?.meta?.resourceType; + const handler = makeDistinctValueHandler(resourceKey, TAGS_DATA_KEY); + return async (...args) => { + try { + groupByTagsState.loading = true; + const results = await handler(...args); + return results; + } catch (e) { + ErrorHandler.handleError(e); + return { results: [] }; + } finally { + groupByTagsState.loading = false; + } + }; +}; + watch([() => props.sourceId, () => props.sourceKey], async () => { resetAllFilter(); }); @@ -225,6 +283,26 @@ watch([ block @select="handleUpdateSelectedGroupBy" /> +
+
+
+ {{ item.label }} tags +
+ +
+
.widget-form-data-table-card-add-form { padding: 0.75rem; + + .groupb-by-tags-area { + @apply bg-gray-100 rounded-lg; + padding: 0.75rem 0.5rem; + margin-top: 0.25rem; + + .tags-dropdown-wrapper { + @apply bg-white border border-gray-150 rounded-lg; + width: 100%; + padding: 0.125rem 0.5rem 0.5rem; + .header-name { + @apply text-label-md font-bold text-gray-800; + height: 2rem; + } + } + } + .data-text-input { @apply w-full; } diff --git a/apps/web/src/common/modules/widgets/_constants/data-table-constant.ts b/apps/web/src/common/modules/widgets/_constants/data-table-constant.ts index 12779cab26..4508f85f8e 100644 --- a/apps/web/src/common/modules/widgets/_constants/data-table-constant.ts +++ b/apps/web/src/common/modules/widgets/_constants/data-table-constant.ts @@ -137,3 +137,14 @@ export const DEFAULT_TRANSFORM_DATA_TABLE_VALUE_MAP = { ], } as ValueMappingOptions, }; + +export const GROUP_BY_INFO_ITEMS_FOR_TAGS = [ + { + key: GROUP_BY.PROJECT, + name: 'project', + }, + { + key: GROUP_BY.SERVICE_ACCOUNT, + name: 'service_account', + }, +] as const; diff --git a/apps/web/src/common/modules/widgets/types/widget-model.ts b/apps/web/src/common/modules/widgets/types/widget-model.ts index f73cea4396..73b18e4946 100644 --- a/apps/web/src/common/modules/widgets/types/widget-model.ts +++ b/apps/web/src/common/modules/widgets/types/widget-model.ts @@ -12,6 +12,13 @@ export type DataTableSourceType = typeof DATA_SOURCE_DOMAIN[keyof typeof DATA_SO export type DataTableOperator = typeof DATA_TABLE_OPERATOR[keyof typeof DATA_TABLE_OPERATOR]; export type DataTableDataType = keyof typeof DATA_TABLE_TYPE; export type AdditionalLabels = Record; +export interface DataTableGroupByInfo { + key:string; + name: string; + reference?: object; + search_key?: string; + tags?: string[]; // [tag_key_1, tag_key_2, ...] +} export interface TimeDiff { years?: number; months?: number; @@ -26,7 +33,7 @@ export type WidgetState = 'CREATING' | 'INACTIVE' | 'ACTIVE'; export interface DataTableAddOptions { 'ASSET'?: AssetOptions; 'COST'?: CostOptions; - group_by?: {key:string; name: string; reference?: object; search_key?: string }[]; + group_by?: DataTableGroupByInfo[]; data_name: string; data_unit?: string; timediff?: TimeDiff; diff --git a/packages/core-lib/src/component-util/query-search/index.ts b/packages/core-lib/src/component-util/query-search/index.ts index 7e2d16f671..4447ea3f21 100644 --- a/packages/core-lib/src/component-util/query-search/index.ts +++ b/packages/core-lib/src/component-util/query-search/index.ts @@ -123,7 +123,7 @@ export function makeDistinctValueHandler( try { const res = await SpaceConnector.client.addOns.autocomplete.distinct(param); - if (keyItem.dataType === 'object') return getHandlerResp(res.results[0]?.key, res.results.map((d) => ({ label: d.name, name: d.key })), res.total_count); + if (keyItem?.dataType === 'object') return getHandlerResp(res.results[0]?.key, res.results.map((d) => ({ label: d.name, name: d.key })), res.total_count); return { results: res.results.reduce((results, d) => {