diff --git a/apps/web/src/api-clients/cost-analysis/cost-query-set/composables/use-cost-query-set-api.ts b/apps/web/src/api-clients/cost-analysis/cost-query-set/composables/use-cost-query-set-api.ts index e69de29bb2..aef7cd8f0e 100644 --- a/apps/web/src/api-clients/cost-analysis/cost-query-set/composables/use-cost-query-set-api.ts +++ b/apps/web/src/api-clients/cost-analysis/cost-query-set/composables/use-cost-query-set-api.ts @@ -0,0 +1,21 @@ +import { SpaceConnector } from '@cloudforet/core-lib/space-connector'; + +import type { ListResponse } from '@/api-clients/_common/schema/api-verbs/list'; +import type { CostQuerySetCreateParameters } from '@/api-clients/cost-analysis/cost-query-set/schema/api-verbs/create'; +import type { CostQuerySetDeleteParameters } from '@/api-clients/cost-analysis/cost-query-set/schema/api-verbs/delete'; +import type { CostQuerySetListParameters } from '@/api-clients/cost-analysis/cost-query-set/schema/api-verbs/list'; +import type { CostQuerySetUpdateParameters } from '@/api-clients/cost-analysis/cost-query-set/schema/api-verbs/update'; +import type { CostQuerySetModel } from '@/api-clients/cost-analysis/cost-query-set/schema/model'; + +export const useCostQuerySetApi = () => { + const actions = { + create: SpaceConnector.clientV2.costAnalysis.costQuerySet.create, + list: SpaceConnector.clientV2.costAnalysis.costQuerySet.list>, + update: SpaceConnector.clientV2.costAnalysis.costQuerySet.update, + delete: SpaceConnector.clientV2.costAnalysis.costQuerySet.delete, + }; + + return { + costQuerySetAPI: actions, + }; +}; diff --git a/apps/web/src/api-clients/cost-analysis/cost-report/schema/type.ts b/apps/web/src/api-clients/cost-analysis/cost-report/schema/type.ts index b9fccf8e27..995ccf24e9 100644 --- a/apps/web/src/api-clients/cost-analysis/cost-report/schema/type.ts +++ b/apps/web/src/api-clients/cost-analysis/cost-report/schema/type.ts @@ -1 +1 @@ -export type CostReportStatus = 'IN_PROGRESS' | 'ADJUSTING' | 'DONE'; +export type CostReportStatus = 'IN_PROGRESS' | 'ADJUSTING' | 'DONE' | 'EXPIRED'; diff --git a/apps/web/src/api-clients/cost-analysis/cost/composables/use-cost-api.ts b/apps/web/src/api-clients/cost-analysis/cost/composables/use-cost-api.ts index e69de29bb2..98d080e22e 100644 --- a/apps/web/src/api-clients/cost-analysis/cost/composables/use-cost-api.ts +++ b/apps/web/src/api-clients/cost-analysis/cost/composables/use-cost-api.ts @@ -0,0 +1,28 @@ +import { SpaceConnector } from '@cloudforet/core-lib/space-connector'; + +import type { AnalyzeResponse } from '@/api-clients/_common/schema/api-verbs/analyze'; +import type { CostAnalyzeParameters } from '@/api-clients/cost-analysis/cost/schema/api-verbs/analyze'; +import type { CostStatParameters } from '@/api-clients/cost-analysis/cost/schema/api-verbs/stat'; + +import type { ListResponse } from '@/lib/variable-models/_base/types'; + +import type { CostAnalyzeRawData } from '@/services/cost-explorer/types/cost-analyze-type'; + +import type { CostModel } from '../schema/model'; + +interface UseCostApiReturn { + costAPI: { + analyze: (params: CostAnalyzeParameters) => Promise>; + stat: (params: CostStatParameters) => Promise>; + } +} +export const useCostApi = (): UseCostApiReturn => { + const actions = { + analyze: SpaceConnector.clientV2.costAnalysis.cost.analyze>, + stat: SpaceConnector.clientV2.costAnalysis.cost.stat>, + }; + + return { + costAPI: actions, + }; +}; diff --git a/apps/web/src/common/components/info-tooltip/InfoTooltip.vue b/apps/web/src/common/components/info-tooltip/InfoTooltip.vue new file mode 100644 index 0000000000..5274de29c2 --- /dev/null +++ b/apps/web/src/common/components/info-tooltip/InfoTooltip.vue @@ -0,0 +1,35 @@ + + + diff --git a/apps/web/src/common/composables/contents-accessibility/index.ts b/apps/web/src/common/composables/contents-accessibility/index.ts index 09228415af..6070915f77 100644 --- a/apps/web/src/common/composables/contents-accessibility/index.ts +++ b/apps/web/src/common/composables/contents-accessibility/index.ts @@ -13,7 +13,7 @@ export const useContentsAccessibility = (menuId: MenuId): UseContentsAccessibili const menuStore = useMenuStore(); const state = reactive({ - visibleContents: computed(() => menuStore.getters.menuList.findIndex((menu) => menu.id === menuId) !== -1), + visibleContents: computed(() => menuStore.getters.activeModeMenuList.findIndex((menu) => menu.id === menuId) !== -1), }); return { diff --git a/apps/web/src/common/modules/monitoring/MetricChart.vue b/apps/web/src/common/modules/monitoring/MetricChart.vue index 0e77af638c..7834a31567 100644 --- a/apps/web/src/common/modules/monitoring/MetricChart.vue +++ b/apps/web/src/common/modules/monitoring/MetricChart.vue @@ -83,6 +83,7 @@ const state = reactive({ }, series: state.chartData, })), + chartDataForDataLoader: computed(() => state.chartData?.filter((item) => !isEmpty(item?.data)) ?? undefined), }); const drawChart = (rawData) => { @@ -125,7 +126,7 @@ useResizeObserver(chartContext, throttle(() => { diff --git a/apps/web/src/common/pages/CostReportDetailPage.vue b/apps/web/src/common/pages/CostReportDetailPage.vue index 79aeee4368..ed7ad2eecd 100644 --- a/apps/web/src/common/pages/CostReportDetailPage.vue +++ b/apps/web/src/common/pages/CostReportDetailPage.vue @@ -1,5 +1,6 @@ - - - - diff --git a/apps/web/src/services/cost-explorer/components/CostAnalysisHeader.vue b/apps/web/src/services/cost-explorer/components/CostAnalysisHeader.vue index 9954219439..b4f6c68010 100644 --- a/apps/web/src/services/cost-explorer/components/CostAnalysisHeader.vue +++ b/apps/web/src/services/cost-explorer/components/CostAnalysisHeader.vue @@ -119,7 +119,7 @@ const handleDeleteQueryConfirm = async () => { const handleRouteToUnifiedCostSettings = () => { router.push({ - name: ADMIN_COST_EXPLORER_ROUTE.COST_ADVANCED_SETTINGS.CURRENCY_CONVERTER._NAME, + name: ADMIN_COST_EXPLORER_ROUTE.COST_ADVANCED_SETTINGS._NAME, }).catch(() => {}); }; diff --git a/apps/web/src/services/cost-explorer/components/CostReportMonthlyTotalAmountSummaryCard.vue b/apps/web/src/services/cost-explorer/components/CostReportMonthlyTotalAmountSummaryCard.vue index 690f1ca669..3a39f9fab5 100644 --- a/apps/web/src/services/cost-explorer/components/CostReportMonthlyTotalAmountSummaryCard.vue +++ b/apps/web/src/services/cost-explorer/components/CostReportMonthlyTotalAmountSummaryCard.vue @@ -10,14 +10,14 @@ import type { PieSeriesOption } from 'echarts/charts'; import type { EChartsType } from 'echarts/core'; import { init } from 'echarts/core'; import { - debounce, isEmpty, sum, sumBy, throttle, + debounce, isEmpty, sum, throttle, } from 'lodash'; import { SpaceConnector } from '@cloudforet/core-lib/space-connector'; import { PSelectButton, PDatePagination, PDataTable, PTextButton, PI, PTooltip, PDataLoader, } from '@cloudforet/mirinae'; -import type { SelectButtonType } from '@cloudforet/mirinae/types/controls/buttons/select-button-group/type'; +import type { MenuItem } from '@cloudforet/mirinae/types/controls/context-menu/type'; import type { DataTableFieldType } from '@cloudforet/mirinae/types/data-display/tables/data-table/type'; import { numberFormatter } from '@cloudforet/utils'; @@ -56,7 +56,15 @@ import { type CostReportDataAnalyzeResult = { [groupBy: string]: string | any; - value_sum: number; + value_sum: Array<{ + [key: string]: number; + }>; + _total_value_sum: number; +}; +type RefinedCostReportDataAnalyzeResult = { + [groupBy: string]: string | any; + amount: number; + adjusted_amount?: number; }; const OTHER_CATEGORY = 'Others'; @@ -75,92 +83,84 @@ const storeState = reactive({ }); const state = reactive({ loading: true, - data: undefined as AnalyzeResponse|undefined, - targetSelectItems: computed(() => ([ - { name: GROUP_BY.WORKSPACE_NAME, label: i18n.t('BILLING.COST_MANAGEMENT.COST_REPORT.WORKSPACE') }, - { name: GROUP_BY.PROVIDER, label: i18n.t('BILLING.COST_MANAGEMENT.COST_REPORT.PROVIDER') }, - ] as SelectButtonType[])), - selectedTarget: storeState.isAdminMode ? GROUP_BY.WORKSPACE_NAME : GROUP_BY.PROVIDER, - totalAmount: computed(() => sum(state.data?.results.map((d) => d.value_sum))), + rawData: undefined as AnalyzeResponse|undefined, + refinedData: [] as RefinedCostReportDataAnalyzeResult[], + selectedTarget: storeState.isAdminMode ? GROUP_BY.WORKSPACE : GROUP_BY.PROVIDER, currentDate: undefined as Dayjs | undefined, - currentDateRangeText: computed(() => { - if (!state.currentDate) return ''; - return `${state.currentDate.startOf('month').format('YYYY-MM-DD')} ~ ${state.currentDate.endOf('month').format('YYYY-MM-DD')}`; - }), currentReportId: undefined as string|undefined, - // chart: null as EChartsType | null, - chartData: [], - chartOptions: computed(() => ({ - color: MASSIVE_CHART_COLORS, - grid: { - containLabel: true, - }, - tooltip: { - trigger: 'item', - position: 'inside', - formatter: (params) => { - const _name = getReferenceLabel(storeState.allReferenceTypeInfo, state.selectedTarget, params.name); - const _value = numberFormatter(params.value) || ''; - return `${params.marker} ${_name}: ${_value}`; - }, - }, - legend: { - show: false, + chartData: [] as PieSeriesOption['data'], +}); + +/* Computed */ +const currentDateRangeText = computed(() => { + if (!state.currentDate) return ''; + return `${state.currentDate.startOf('month').format('YYYY-MM-DD')} ~ ${state.currentDate.endOf('month').format('YYYY-MM-DD')}`; +}); +const totalAmount = computed(() => sum(state.rawData?.results?.map((d) => d._total_value_sum) ?? [])); +const targetSelectItems = computed(() => ([ + { name: GROUP_BY.WORKSPACE, label: i18n.t('BILLING.COST_MANAGEMENT.COST_REPORT.WORKSPACE') }, + { name: GROUP_BY.PROVIDER, label: i18n.t('BILLING.COST_MANAGEMENT.COST_REPORT.PROVIDER') }, +])); +const chartOptions = computed(() => ({ + color: MASSIVE_CHART_COLORS, + grid: { + containLabel: true, + }, + tooltip: { + trigger: 'item', + position: 'inside', + formatter: (params) => { + const _name = getReferenceLabel(storeState.allReferenceTypeInfo, state.selectedTarget, params.name); + const _value = numberFormatter(params.value) || ''; + return `${params.marker} ${_name}: ${_value}`; }, - series: [ - { - type: 'pie', - radius: ['30%', '70%'], - center: ['30%', '50%'], - data: state.chartData, - emphasis: { - itemStyle: { - shadowBlur: 10, - shadowOffsetX: 0, - shadowColor: 'rgba(0, 0, 0, 0.5)', - }, - }, - avoidLabelOverlap: false, - label: { - show: false, + }, + legend: { + show: false, + }, + series: [ + { + type: 'pie', + radius: ['30%', '70%'], + center: ['30%', '50%'], + data: state.chartData, + emphasis: { + itemStyle: { + shadowBlur: 10, + shadowOffsetX: 0, + shadowColor: 'rgba(0, 0, 0, 0.5)', }, }, - ], - })), - tableFields: computed(() => ([ - { name: state.selectedTarget, label: COST_REPORT_GROUP_BY_ITEM_MAP[state.selectedTarget].label }, - { name: 'value_sum', label: 'Amount', textAlign: 'right' }, - ])), -}); + avoidLabelOverlap: false, + label: { + show: false, + }, + }, + ], +})); +const tableFields = computed(() => ([ + { name: state.selectedTarget, label: COST_REPORT_GROUP_BY_ITEM_MAP[state.selectedTarget].label }, + { name: 'amount', label: 'Amount', textAlign: 'right' }, + { name: 'adjusted_amount', label: 'Adjustment', textAlign: 'right' }, +])); + /* Util */ -const getRefinedAnalyzeData = (res: AnalyzeResponse): AnalyzeResponse => { - const _results: CostReportDataAnalyzeResult[] = []; - const _totalAmount = sumBy(res.results, 'value_sum'); - const _thresholdValue = _totalAmount * 0.02; - let _othersValueSum = 0; +const getRefinedAnalyzeData = (res: AnalyzeResponse): RefinedCostReportDataAnalyzeResult[] => { + const _results: RefinedCostReportDataAnalyzeResult[] = []; res.results?.forEach((d) => { - if (d.value_sum < _thresholdValue) { - _othersValueSum += d.value_sum; - } else { - _results.push(d); - } - }); - if (_othersValueSum > 0) { _results.push({ - [state.selectedTarget]: OTHER_CATEGORY, - value_sum: _othersValueSum, + [state.selectedTarget]: d[state.selectedTarget], + amount: d._total_value_sum, + adjusted_amount: d.value_sum.find((v) => !!v?.is_adjusted)?.value, }); - } - return { - more: res.more, - results: _results, - }; + }); + return _results; }; const getLegendColor = (field: string, value: string, rowIndex: number) => { if (value === OTHER_CATEGORY) return gray[500]; - if (field === GROUP_BY.WORKSPACE_NAME) { + if (field === GROUP_BY.WORKSPACE) { return MASSIVE_CHART_COLORS[rowIndex]; } return storeState.providers[value]?.color ?? MASSIVE_CHART_COLORS[rowIndex]; @@ -178,7 +178,7 @@ const analyzeCostReportData = debounce(async () => { cost_report_config_id: costReportPageState.costReportConfig?.cost_report_config_id, is_confirmed: true, query: { - group_by: [state.selectedTarget], + group_by: [state.selectedTarget, 'is_adjusted'], start: _period.start, end: _period.end, fields: { @@ -187,15 +187,17 @@ const analyzeCostReportData = debounce(async () => { operator: 'sum', }, }, + field_group: ['is_adjusted'], sort: [{ key: 'value_sum', desc: true, }], }, }); - state.data = getRefinedAnalyzeData(res); + state.rawData = res; + state.refinedData = getRefinedAnalyzeData(res); } catch (e) { - state.data = {}; + state.rawData = { results: [] }; ErrorHandler.handleError(e); } finally { state.loading = false; @@ -220,16 +222,17 @@ const listCostReport = async () => { }; /* Util */ -const drawChart = (rawData: AnalyzeResponse) => { +const drawChart = (rawData?: AnalyzeResponse) => { if (isEmpty(rawData)) return; - const _seriesData: any[] = []; + const _seriesData: PieSeriesOption['data'] = []; rawData.results?.forEach((d) => { - let _color = state.selectedTarget === 'provider' ? storeState.providers[d[state.selectedTarget]]?.color : undefined; + let _color = state.selectedTarget === GROUP_BY.PROVIDER ? storeState.providers[d[state.selectedTarget]]?.color : undefined; if (d[state.selectedTarget] === OTHER_CATEGORY) _color = gray[500]; + const _value = sum(d.value_sum.map((v) => v.value)); _seriesData.push({ name: d[state.selectedTarget], - value: d.value_sum, + value: _value, itemStyle: { color: _color, }, @@ -238,7 +241,7 @@ const drawChart = (rawData: AnalyzeResponse) => { state.chartData = _seriesData; state.chart = init(chartContext.value); - state.chart.setOption(state.chartOptions, true); + state.chart.setOption(chartOptions.value, true); }; /* Event */ @@ -262,9 +265,9 @@ const handleChangeDate = (date: Dayjs) => { }; /* Watcher */ -watch([() => chartContext.value, () => state.loading, () => state.data], async ([_chartContext, loading, data]) => { +watch([() => chartContext.value, () => state.loading, () => state.rawData], async ([_chartContext, loading, rawData]) => { if (_chartContext && !loading) { - drawChart(data); + drawChart(rawData); } }); watch(() => costReportPageState.recentReportMonth, async (after) => { @@ -297,7 +300,7 @@ useResizeObserver(chartContext, throttle(() => { #right-extra >
- { - -