diff --git a/apps/web/src/common/modules/widgets/_components/WidgetFormDataSourcePopover.vue b/apps/web/src/common/modules/widgets/_components/WidgetFormDataSourcePopover.vue index a8e776b2dc..0ce69f54b1 100644 --- a/apps/web/src/common/modules/widgets/_components/WidgetFormDataSourcePopover.vue +++ b/apps/web/src/common/modules/widgets/_components/WidgetFormDataSourcePopover.vue @@ -30,6 +30,8 @@ import ErrorHandler from '@/common/composables/error/errorHandler'; import WidgetFormAssetSecurityDataSourcePopper from '@/common/modules/widgets/_components/WidgetFormAssetSecurityDataSourcePopper.vue'; import WidgetFormCostDataSourcePopper from '@/common/modules/widgets/_components/WidgetFormCostDataSourcePopper.vue'; +import WidgetFormUnifiedCostDataSourcePopper + from '@/common/modules/widgets/_components/WidgetFormUnifiedCostDataSourcePopper.vue'; import { useWidgetFormQuery } from '@/common/modules/widgets/_composables/use-widget-form-query'; import { DATA_SOURCE_DOMAIN, @@ -104,6 +106,9 @@ const state = reactive({ if (state.selectedDataSourceDomain === DATA_SOURCE_DOMAIN.COST) { return !state.selectedCostDataSourceId || !state.selectedCostDataType; } + if (state.selectedDataSourceDomain === DATA_SOURCE_DOMAIN.UNIFIED_COST) { + return !state.selectedUnifiedCostDataType; + } if ([DATA_SOURCE_DOMAIN.ASSET, DATA_SOURCE_DOMAIN.SECURITY].includes(state.selectedDataSourceDomain)) { return !state.selectedMetricId; } @@ -112,6 +117,7 @@ const state = reactive({ // cost selectedCostDataSourceId: undefined as undefined|string, selectedCostDataType: undefined as undefined|string, + selectedUnifiedCostDataType: undefined as undefined|string, // asset & security selectedMetricId: undefined as undefined|string, selectedNamespace: computed(() => { @@ -236,6 +242,7 @@ const { mutateAsync: addDataTable, isPending: dataTableAddLoading } = useMutatio const resetSelectedDataSource = () => { state.selectedCostDataSourceId = undefined; state.selectedCostDataType = undefined; + state.selectedUnifiedCostDataType = undefined; state.selectedMetricId = undefined; }; @@ -287,9 +294,11 @@ const handleConfirmDataSource = async () => { } if (state.selectedPopperCondition === DATA_TABLE_TYPE.ADDED) { - const dataTableBaseName = state.selectedDataSourceDomain === DATA_SOURCE_DOMAIN.COST - ? `${storeState.costDataSources[state.selectedCostDataSourceId].name} - ${state.selectedCostDataTypeLabel}` - : `${state.selectedNamespace.name} - ${storeState.metrics[state.selectedMetricId]?.label}`; + let dataTableBaseName: string; + if (state.selectedDataSourceDomain === DATA_SOURCE_DOMAIN.COST) dataTableBaseName = `${storeState.costDataSources[state.selectedCostDataSourceId].name} - ${state.selectedCostDataTypeLabel}`; + else if (state.selectedDataSourceDomain === DATA_SOURCE_DOMAIN.UNIFIED_COST) dataTableBaseName = 'Unified Cost'; + else dataTableBaseName = `${state.selectedNamespace.name} - ${storeState.metrics[state.selectedMetricId]?.label}`; + const addParameters = { widget_id: widgetGenerateState.widgetId as string, source_type: state.selectedDataSourceDomain, @@ -313,7 +322,13 @@ const handleConfirmDataSource = async () => { data_key: state.selectedCostDataType, }, }; - const assetOptions = { + const unifiedCostOptions: DataTableAddOptions = { + data_name: dataTableBaseName, + UNIFIED_COST: { + data_key: state.selectedUnifiedCostDataType, + }, + }; + const assetOptions: DataTableAddOptions = { data_name: storeState.metrics[state.selectedMetricId]?.label, data_unit: storeState.metrics[state.selectedMetricId]?.data.unit, ASSET: { @@ -325,13 +340,19 @@ const handleConfirmDataSource = async () => { widgetGenerateStore.setDataTableCreateLoading(true); state.showPopover = false; - await addDataTable({ + const mergedParams = { ...addParameters, vars: dashboard.value?.vars || {}, - options: { - ...state.selectedDataSourceDomain === DATA_SOURCE_DOMAIN.COST ? costOptions : assetOptions, - }, - }); + }; + if (state.selectedDataSourceDomain === DATA_SOURCE_DOMAIN.COST) { + mergedParams.options = costOptions; + } else if (state.selectedDataSourceDomain === DATA_SOURCE_DOMAIN.UNIFIED_COST) { + mergedParams.options = unifiedCostOptions; + } else { + mergedParams.options = assetOptions; + } + + await addDataTable(mergedParams); emit('scroll'); } widgetGenerateStore.setDataTableCreateLoading(false); @@ -413,6 +434,29 @@ watch(() => state.showPopover, (val) => {

+ +
+ +
+ +
+

+ {{ i18n.t('DASHBOARDS.WIDGET.OVERLAY.STEP_1.UNIFIED_COST') }} +

+
+
+

{{ i18n.t('DASHBOARDS.WIDGET.OVERLAY.STEP_1.INVENTORY') }}

@@ -445,6 +489,9 @@ watch(() => state.showPopover, (val) => { :selected-cost-data-source-id.sync="state.selectedCostDataSourceId" :selected-cost-data-type.sync="state.selectedCostDataType" /> + state.showPopover, (val) => { .data-source-popover-content { display: flex; flex-direction: column; - min-width: 43.5rem; + min-width: 44rem; height: 26rem; .top-part { display: flex; @@ -657,7 +704,7 @@ watch(() => state.showPopover, (val) => { @apply border-r border-gray-200; display: flex; flex-direction: column; - min-width: 11.5rem; + min-width: 12rem; height: 100%; padding: 1rem 0.75rem; diff --git a/apps/web/src/common/modules/widgets/_components/WidgetFormDataTableCardAddContents.vue b/apps/web/src/common/modules/widgets/_components/WidgetFormDataTableCardAddContents.vue index b6b67531db..149e8d3604 100644 --- a/apps/web/src/common/modules/widgets/_components/WidgetFormDataTableCardAddContents.vue +++ b/apps/web/src/common/modules/widgets/_components/WidgetFormDataTableCardAddContents.vue @@ -143,7 +143,7 @@ const state = reactive({ }), filterFormKey: getRandomId(), optionsChanged: computed(() => { - const sourceKeyChanged = state.selectedSourceEndItem !== originDataState.sourceKey; + const sourceKeyChanged = state.sourceType !== DATA_SOURCE_DOMAIN.UNIFIED_COST && 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); @@ -175,7 +175,11 @@ const validationState = reactive({ }); const originDataState = reactive({ - sourceKey: computed(() => (state.sourceType === DATA_SOURCE_DOMAIN.COST ? props.item.options[DATA_SOURCE_DOMAIN.COST]?.data_key : props.item.options[DATA_SOURCE_DOMAIN.ASSET]?.metric_id)), + sourceKey: computed(() => { + if (state.sourceType === DATA_SOURCE_DOMAIN.COST) return props.item.options[DATA_SOURCE_DOMAIN.COST]?.data_key; + if (state.sourceType === DATA_SOURCE_DOMAIN.UNIFIED_COST) return 'cost'; + return props.item.options[DATA_SOURCE_DOMAIN.ASSET]?.metric_id; + }), groupBy: computed(() => ((props.item.options as DataTableAddOptions).group_by ?? []).map((group) => ({ name: group.key, label: group.name, @@ -302,9 +306,10 @@ const updateDataTable = async (): Promise => { return undefined; } - const domainOptions = state.sourceType === DATA_SOURCE_DOMAIN.COST - ? { data_source_id: state.dataSourceId, data_key: state.selectedSourceEndItem } - : { metric_id: state.selectedSourceEndItem }; + let domainOptions; + if (state.sourceType === DATA_SOURCE_DOMAIN.COST) domainOptions = { data_source_id: state.dataSourceId, data_key: state.selectedSourceEndItem }; + if (state.sourceType === DATA_SOURCE_DOMAIN.UNIFIED_COST) domainOptions = { data_key: 'cost' }; + if (state.sourceType === DATA_SOURCE_DOMAIN.ASSET) domainOptions = { metric_id: state.selectedSourceEndItem }; const costGroupBy = state.selectedGroupByItems.map((group) => ({ key: group.name, @@ -313,7 +318,8 @@ const updateDataTable = async (): Promise => { })); 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; + + const groupBy = (state.sourceType === DATA_SOURCE_DOMAIN.COST || state.sourceType === DATA_SOURCE_DOMAIN.UNIFIED_COST) ? costGroupBy : assetGroupBy; GROUP_BY_INFO_ITEMS_FOR_TAGS.forEach((tag) => { const groupByTags = groupBy.find((group) => group.key === tag.key); @@ -532,7 +538,8 @@ defineExpose({ :selected="props.selected" :data-table-name.sync="dataTableNameState.dataTableName" /> - ; @@ -87,7 +88,7 @@ const storeState = reactive({ }); const { allItems: costDataSourceMenuItems } = useCostDataSourceFilterMenuItems({ isAdminMode: computed(() => storeState.isAdminMode), - costDataSource: computed(() => storeState.costDataSources[props.sourceId]), + costDataSource: computed(() => storeState.costDataSources[props.sourceId ?? '']), }); const state = reactive({ @@ -149,12 +150,20 @@ const groupByState = reactive({ if (props.sourceType === DATA_SOURCE_DOMAIN.COST) { return costDataSourceMenuItems.value.filter((d) => d.name !== 'project_group_id'); } + if (props.sourceType === DATA_SOURCE_DOMAIN.UNIFIED_COST) { + const groupByItemValueList = Object.values(GROUP_BY_ITEM_MAP); + if (!storeState.isAdminMode) return groupByItemValueList.filter((d) => d.name !== 'workspace_id').map((d) => ({ name: d.name, label: d.label })); + return groupByItemValueList.map((d) => ({ name: d.name, label: d.label })); + } return [...assetFilterState.metricItems]; }), - filterItems: computed(() => { + filterItems: computed(() => { if (props.sourceType === DATA_SOURCE_DOMAIN.COST) { return costDataSourceMenuItems.value; } + if (props.sourceType === DATA_SOURCE_DOMAIN.UNIFIED_COST) { + return groupByState.items; + } return [...assetFilterState.metricFilterItems]; }), }); diff --git a/apps/web/src/common/modules/widgets/_components/WidgetFormDataTableCardFilters.vue b/apps/web/src/common/modules/widgets/_components/WidgetFormDataTableCardFilters.vue index f05275400b..ee7ec55229 100644 --- a/apps/web/src/common/modules/widgets/_components/WidgetFormDataTableCardFilters.vue +++ b/apps/web/src/common/modules/widgets/_components/WidgetFormDataTableCardFilters.vue @@ -105,9 +105,12 @@ const state = reactive({ proxyFilter: useProxyValue>('filter', props, emit), filterItems: computed(() => props.filterItems), selectedItems: [] as any[], + primaryCostOptions: computed>(() => ({ + ...((props.sourceType === DATA_SOURCE_DOMAIN.COST && props.sourceId) ? { data_source_id: props.sourceId } : {}), + })), handlerMap: computed(() => { const handlerMaps = {}; - if (props.sourceType === DATA_SOURCE_DOMAIN.COST) { + if (props.sourceType === DATA_SOURCE_DOMAIN.COST || props.sourceType === DATA_SOURCE_DOMAIN.UNIFIED_COST) { state.selectedItems.forEach(({ name, presetKeys }) => { handlerMaps[name] = getCostMenuHandler(name, { presetKeys }); }); @@ -118,9 +121,6 @@ const state = reactive({ } return handlerMaps; }), - primaryCostOptions: computed>(() => ({ - data_source_id: props.sourceId, - })), primaryMetricStatOptions: computed>(() => ({ metric_id: props.sourceId, })), diff --git a/apps/web/src/common/modules/widgets/_components/WidgetFormOverlayStep2.vue b/apps/web/src/common/modules/widgets/_components/WidgetFormOverlayStep2.vue index 5b181b7b56..ed048df910 100644 --- a/apps/web/src/common/modules/widgets/_components/WidgetFormOverlayStep2.vue +++ b/apps/web/src/common/modules/widgets/_components/WidgetFormOverlayStep2.vue @@ -179,10 +179,12 @@ const updateWidget = async () => { if (_layouts.length) { const _targetLayout = _layouts[0]; if (_targetLayout.widgets) { - _targetLayout.widgets.push(widgetGenerateState.widgetId); + const newLayoutWidgets = [..._targetLayout.widgets as string[], widgetGenerateState.widgetId]; + _targetLayout.widgets = sanitizeAndSortWidgets(newLayoutWidgets, widgetList.value.map((w) => w.widget_id)); } else { _targetLayout.widgets = [widgetGenerateState.widgetId]; } + _layouts[0] = _targetLayout; } else { _layouts.push({ @@ -208,6 +210,28 @@ const { mutate: updateDashboard } = useMutation( ); /* Util */ +const sanitizeAndSortWidgets = (_layoutWidgets: string[] = [], _widgetList: string[] = []): string[] => { + const uniqueWidgets = [...new Set(_layoutWidgets)]; + + if (uniqueWidgets.length === _widgetList.length && uniqueWidgets.every((item) => _widgetList.includes(item))) { + return [...uniqueWidgets]; + } + + const widgetsSet = new Set(uniqueWidgets); + const widgetListSet = new Set(_widgetList); + + const missingInWidgets = [...widgetListSet].filter((item) => !widgetsSet.has(item)); + + const sanitizedWidgets = uniqueWidgets.filter((item) => widgetListSet.has(item)).concat(missingInWidgets); + + const indexMap = new Map(_widgetList.map((item, index) => [item, index])); + sanitizedWidgets.sort((a, b) => (indexMap.get(a) ?? Infinity) - (indexMap.get(b) ?? Infinity)); + + + return sanitizedWidgets; +}; + + const initSnapshot = () => { state.varsSnapshot = cloneDeep(dashboard.value?.vars || {}); state.dashboardOptionsSnapshot = cloneDeep(dashboardDetailState.options); diff --git a/apps/web/src/common/modules/widgets/_components/WidgetFormUnifiedCostDataSourcePopper.vue b/apps/web/src/common/modules/widgets/_components/WidgetFormUnifiedCostDataSourcePopper.vue new file mode 100644 index 0000000000..73a0fdd730 --- /dev/null +++ b/apps/web/src/common/modules/widgets/_components/WidgetFormUnifiedCostDataSourcePopper.vue @@ -0,0 +1,62 @@ + + + + + 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 de92e36c7d..01605147ab 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 @@ -16,6 +16,7 @@ export const DATA_TABLE_TYPE = { export const DATA_SOURCE_DOMAIN = { COST: 'COST', + UNIFIED_COST: 'UNIFIED_COST', ASSET: 'ASSET', SECURITY: 'SECURITY', }; 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 73b18e4946..09ecb628ca 100644 --- a/apps/web/src/common/modules/widgets/types/widget-model.ts +++ b/apps/web/src/common/modules/widgets/types/widget-model.ts @@ -33,6 +33,7 @@ export type WidgetState = 'CREATING' | 'INACTIVE' | 'ACTIVE'; export interface DataTableAddOptions { 'ASSET'?: AssetOptions; 'COST'?: CostOptions; + 'UNIFIED_COST'?: UnifiedCostOptions; group_by?: DataTableGroupByInfo[]; data_name: string; data_unit?: string; @@ -52,6 +53,10 @@ export interface CostOptions { data_key: string; } +export interface UnifiedCostOptions { + data_key: string; +} + /* TRANSFORM Data Type Options */ export interface DataTableTransformOptions { CONCAT?: ConcatOptions; diff --git a/packages/language-pack/console-translation-2.8.babel b/packages/language-pack/console-translation-2.8.babel index 82b1c3346c..543581d0e5 100644 --- a/packages/language-pack/console-translation-2.8.babel +++ b/packages/language-pack/console-translation-2.8.babel @@ -45918,6 +45918,27 @@ + + UNIFIED_COST + false + + + + + + en-US + false + + + ja-JP + false + + + ko-KR + false + + + diff --git a/packages/language-pack/en.json b/packages/language-pack/en.json index b70910d267..8778e7fcb9 100644 --- a/packages/language-pack/en.json +++ b/packages/language-pack/en.json @@ -2522,7 +2522,8 @@ "NAMESPACE": "Namespace", "SECURITY": "Security", "SELECT_A_DATA_SOURCE": "Select a data source", - "SELECT_DATA_SOURCE_TYPE": "Select Data Source Type" + "SELECT_DATA_SOURCE_TYPE": "Select Data Source Type", + "UNIFIED_COST": "Unified Cost" }, "STEP_2": { "ADD_RULE": "Add Rule", diff --git a/packages/language-pack/ja.json b/packages/language-pack/ja.json index c92715e0d4..be5be3652f 100644 --- a/packages/language-pack/ja.json +++ b/packages/language-pack/ja.json @@ -2522,7 +2522,8 @@ "NAMESPACE": "Namespace", "SECURITY": "セキュリティ", "SELECT_A_DATA_SOURCE": "データソースを選択", - "SELECT_DATA_SOURCE_TYPE": "データソースの種類を選択" + "SELECT_DATA_SOURCE_TYPE": "データソースの種類を選択", + "UNIFIED_COST": "" }, "STEP_2": { "ADD_RULE": "ルールを追加", diff --git a/packages/language-pack/ko.json b/packages/language-pack/ko.json index 875f6e411b..19bb936ef5 100644 --- a/packages/language-pack/ko.json +++ b/packages/language-pack/ko.json @@ -2522,7 +2522,8 @@ "NAMESPACE": "네임스페이스", "SECURITY": "보안", "SELECT_A_DATA_SOURCE": "데이터 소스 선택", - "SELECT_DATA_SOURCE_TYPE": "데이터 소스 타입 선택" + "SELECT_DATA_SOURCE_TYPE": "데이터 소스 타입 선택", + "UNIFIED_COST": "" }, "STEP_2": { "ADD_RULE": "규칙 추가",