diff --git a/.github/workflows/dispatch_deploy_unfied_storybook.yaml b/.github/workflows/dispatch_deploy_unfied_storybook.yaml new file mode 100644 index 0000000000..a8cfb7a2ca --- /dev/null +++ b/.github/workflows/dispatch_deploy_unfied_storybook.yaml @@ -0,0 +1,31 @@ +name: "[Dispatch] Deploy unified storybook" + +on: + workflow_dispatch: + +env: + TEAM_NAME: ${{ vars.VERCEL_TEAM_NAME }} + PROJECT_NAME: "unified-storybook" + VERCEL_TOKEN: ${{ secrets.VERCEL_CLOUDFORET_TOKEN }} + +jobs: + Deploy-Production: + runs-on: ubuntu-latest + steps: + - name: Checkout code and submodules + uses: actions/checkout@v2 + with: + submodules: true + fetch-depth: 0 + + - name: Set up environment + run: sudo apt-get install jq + + - name: Install Vercel CLI + run: npm install --global vercel@latest + + - name: Checkout Vercel Target Project + run: vercel link --yes --scope=${{ env.TEAM_NAME }} --project=${{ env.PROJECT_NAME }} --token=${{ env.VERCEL_TOKEN }} + + - name: Deploy Project Artifacts to Vercel + run: vercel deploy --prod --token=${{ env.VERCEL_TOKEN }} diff --git a/.github/workflows/pull_request_review_state_labeler.yaml b/.github/workflows/pull_request_review_state_labeler.yaml index 7e8036b829..caeccb1872 100644 --- a/.github/workflows/pull_request_review_state_labeler.yaml +++ b/.github/workflows/pull_request_review_state_labeler.yaml @@ -18,7 +18,7 @@ jobs: id: check_self_approved uses: actions/github-script@v6 with: - github-token: ${{ secrets.GITHUB_TOKEN }} + github-token: ${{ secrets.CONSOLE_PR_PAT_TOKEN }} script: | const prNumber = context.payload.pull_request.number; const owner = context.repo.owner; @@ -36,7 +36,7 @@ jobs: if: github.event_name == 'pull_request_target' && steps.check_self_approved.outputs.isSelfApprovedPR == 'false' uses: actions/github-script@v6 with: - github-token: ${{ secrets.GITHUB_TOKEN }} + github-token: ${{ secrets.CONSOLE_PR_PAT_TOKEN }} script: | const prNumber = context.payload.pull_request.number; const owner = context.repo.owner; @@ -56,7 +56,7 @@ jobs: if: github.event_name == 'pull_request_review' && steps.check_self_approved.outputs.isSelfApprovedPR == 'false' uses: actions/github-script@v6 with: - github-token: ${{ secrets.GITHUB_TOKEN }} + github-token: ${{ secrets.CONSOLE_PR_PAT_TOKEN }} script: | const prNumber = context.payload.pull_request.number; const owner = context.repo.owner; diff --git a/apps/unified-storybook/.storybook/main.ts b/apps/unified-storybook/.storybook/main.ts index 9832c9d050..e1e7fce5b7 100644 --- a/apps/unified-storybook/.storybook/main.ts +++ b/apps/unified-storybook/.storybook/main.ts @@ -23,11 +23,11 @@ const config: StorybookConfig = { return { mirinae: { title: 'Mirinae Component', - url: 'https://your-production-react-storybook-url', + url: 'https://storybook.developer.spaceone.dev', }, web: { title: 'Web Component', - url: 'https://your-production-angular-storybook-url', + url: 'https://spaceone-web-storybook.vercel.app', }, }; }, diff --git a/apps/web-storybook/.gitignore b/apps/web-storybook/.gitignore index 7a7f1e3c25..b2c4fdd1ad 100755 --- a/apps/web-storybook/.gitignore +++ b/apps/web-storybook/.gitignore @@ -1,3 +1,4 @@ # Transpiled code .out storybook-static +.vercel 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/_composables/use-widget-frame.ts b/apps/web/src/common/modules/widgets/_composables/use-widget-frame.ts index 5bd66cf863..d24d672efd 100644 --- a/apps/web/src/common/modules/widgets/_composables/use-widget-frame.ts +++ b/apps/web/src/common/modules/widgets/_composables/use-widget-frame.ts @@ -25,8 +25,6 @@ import type { DateRange } from '@/common/modules/widgets/types/widget-data-type' import type { WidgetEmit, WidgetProps, WidgetSize, } from '@/common/modules/widgets/types/widget-display-type'; -import type { WidgetFieldName } from '@/common/modules/widgets/types/widget-field-type'; -import type { WidgetFieldValues } from '@/common/modules/widgets/types/widget-field-value-type'; import type { FullDataLink, WidgetFrameProps } from '@/common/modules/widgets/types/widget-frame-type'; import { ASSET_INVENTORY_ROUTE_V1 } from '@/services/asset-inventory-v1/routes/route-constant'; @@ -86,12 +84,16 @@ const getRecursiveDataTableIds = (prevValue: string[] = [], dataTable: DataTable _results = _results.concat(..._dataTables.map((d) => getRecursiveDataTableIds(prevValue, d, dataTables))); return _results; } - return prevValue.concat(dataTable.data_table_id); + + const _dataTableId = dataTable?.options?.[dataTable.operator]?.data_table_id; + const _dataTable = dataTables.find((d) => d.data_table_id === _dataTableId); + _results = _results.concat(...getRecursiveDataTableIds(prevValue, _dataTable, dataTables)); + return _results; } return prevValue.concat(dataTable.data_table_id); }; -const getFullDataLocation = (dataTable: DataTableModel, widgetOptions?: Record, dateRange?: DateRange, dashboardVars?: DashboardVars): Location|undefined => { - const _granularity = (widgetOptions?.granularity as GranularityValue).granularity || 'MONTHLY'; +const getFullDataLocation = (dataTable: DataTableModel, widgetOptions?: WidgetProps['widgetOptions'], dateRange?: DateRange, dashboardVars?: DashboardVars): Location|undefined => { + const _granularity = (widgetOptions?.granularity?.value as GranularityValue)?.granularity || 'MONTHLY'; const _groupBy: string[] = dataTable?.options?.group_by?.map((d) => d.key); const _costFilters = [ ...(dataTable?.options?.filter ?? []), 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/_widgets/clustered-column-chart/ClusteredColumnChart.vue b/apps/web/src/common/modules/widgets/_widgets/clustered-column-chart/ClusteredColumnChart.vue index 554e604974..d3434610ad 100644 --- a/apps/web/src/common/modules/widgets/_widgets/clustered-column-chart/ClusteredColumnChart.vue +++ b/apps/web/src/common/modules/widgets/_widgets/clustered-column-chart/ClusteredColumnChart.vue @@ -220,7 +220,7 @@ const queryResult = useQuery({ staleTime: WIDGET_LOAD_STALE_TIME, }); -const widgetLoading = computed(() => queryResult.isFetching.value || queryResult.isPending.value); +const widgetLoading = computed(() => queryResult.isFetching.value); const errorMessage = computed(() => { if (!state.dataTable) return i18n.t('COMMON.WIDGETS.NO_DATA_TABLE_ERROR_MESSAGE'); return queryResult.error?.value?.message; diff --git a/apps/web/src/common/modules/widgets/_widgets/color-coded-heatmap/ColorCodedHeatmap.vue b/apps/web/src/common/modules/widgets/_widgets/color-coded-heatmap/ColorCodedHeatmap.vue index 8d52454a60..573ff65699 100644 --- a/apps/web/src/common/modules/widgets/_widgets/color-coded-heatmap/ColorCodedHeatmap.vue +++ b/apps/web/src/common/modules/widgets/_widgets/color-coded-heatmap/ColorCodedHeatmap.vue @@ -120,7 +120,7 @@ const queryResult = useQuery({ staleTime: WIDGET_LOAD_STALE_TIME, }); -const widgetLoading = computed(() => queryResult.isFetching.value || queryResult.isPending.value); +const widgetLoading = computed(() => queryResult.isFetching.value); const errorMessage = computed(() => { if (!state.dataTable) return i18n.t('COMMON.WIDGETS.NO_DATA_TABLE_ERROR_MESSAGE'); return queryResult.error?.value?.message; diff --git a/apps/web/src/common/modules/widgets/_widgets/color-coded-table-heatmap/ColorCodedTableHeatmap.vue b/apps/web/src/common/modules/widgets/_widgets/color-coded-table-heatmap/ColorCodedTableHeatmap.vue index 59c89af386..f675093c44 100644 --- a/apps/web/src/common/modules/widgets/_widgets/color-coded-table-heatmap/ColorCodedTableHeatmap.vue +++ b/apps/web/src/common/modules/widgets/_widgets/color-coded-table-heatmap/ColorCodedTableHeatmap.vue @@ -153,7 +153,7 @@ const queryResult = useQuery({ staleTime: WIDGET_LOAD_STALE_TIME, }); -const widgetLoading = computed(() => queryResult.isFetching.value || queryResult.isPending.value); +const widgetLoading = computed(() => queryResult.isFetching.value); const errorMessage = computed(() => { if (!state.dataTable) return i18n.t('COMMON.WIDGETS.NO_DATA_TABLE_ERROR_MESSAGE'); return queryResult.error?.value?.message; diff --git a/apps/web/src/common/modules/widgets/_widgets/gauge/Gauge.vue b/apps/web/src/common/modules/widgets/_widgets/gauge/Gauge.vue index d37eb74f3b..b672382f81 100644 --- a/apps/web/src/common/modules/widgets/_widgets/gauge/Gauge.vue +++ b/apps/web/src/common/modules/widgets/_widgets/gauge/Gauge.vue @@ -177,7 +177,7 @@ const queryResult = useQuery({ staleTime: WIDGET_LOAD_STALE_TIME, }); -const widgetLoading = computed(() => queryResult.isFetching.value || queryResult.isPending.value); +const widgetLoading = computed(() => queryResult.isFetching.value); const errorMessage = computed(() => { if (!state.dataTable) return i18n.t('COMMON.WIDGETS.NO_DATA_TABLE_ERROR_MESSAGE'); return queryResult.error?.value?.message; diff --git a/apps/web/src/common/modules/widgets/_widgets/geo-map/GeoMap.vue b/apps/web/src/common/modules/widgets/_widgets/geo-map/GeoMap.vue index ca055b2ac4..77bdabf3ba 100644 --- a/apps/web/src/common/modules/widgets/_widgets/geo-map/GeoMap.vue +++ b/apps/web/src/common/modules/widgets/_widgets/geo-map/GeoMap.vue @@ -156,7 +156,7 @@ const queryResult = useQuery({ staleTime: WIDGET_LOAD_STALE_TIME, }); -const widgetLoading = computed(() => queryResult.isFetching.value || queryResult.isPending.value); +const widgetLoading = computed(() => queryResult.isFetching.value); const errorMessage = computed(() => { if (!state.dataTable) return i18n.t('COMMON.WIDGETS.NO_DATA_TABLE_ERROR_MESSAGE'); return queryResult.error?.value?.message; diff --git a/apps/web/src/common/modules/widgets/_widgets/heatmap/Heatmap.vue b/apps/web/src/common/modules/widgets/_widgets/heatmap/Heatmap.vue index f01c01623f..24882737f1 100644 --- a/apps/web/src/common/modules/widgets/_widgets/heatmap/Heatmap.vue +++ b/apps/web/src/common/modules/widgets/_widgets/heatmap/Heatmap.vue @@ -219,7 +219,7 @@ const queryResult = useQuery({ staleTime: WIDGET_LOAD_STALE_TIME, }); -const widgetLoading = computed(() => queryResult.isFetching.value || queryResult.isPending.value); +const widgetLoading = computed(() => queryResult.isFetching.value); const errorMessage = computed(() => { if (!state.dataTable) return i18n.t('COMMON.WIDGETS.NO_DATA_TABLE_ERROR_MESSAGE'); return queryResult.error?.value?.message; diff --git a/apps/web/src/common/modules/widgets/_widgets/line-chart/LineChart.vue b/apps/web/src/common/modules/widgets/_widgets/line-chart/LineChart.vue index 55579d73ec..e91741a7d2 100644 --- a/apps/web/src/common/modules/widgets/_widgets/line-chart/LineChart.vue +++ b/apps/web/src/common/modules/widgets/_widgets/line-chart/LineChart.vue @@ -225,7 +225,7 @@ const queryResult = useQuery({ staleTime: WIDGET_LOAD_STALE_TIME, }); -const widgetLoading = computed(() => queryResult.isFetching.value || queryResult.isPending.value); +const widgetLoading = computed(() => queryResult.isFetching.value); const errorMessage = computed(() => { if (!state.dataTable) return i18n.t('COMMON.WIDGETS.NO_DATA_TABLE_ERROR_MESSAGE'); return queryResult.error?.value?.message; diff --git a/apps/web/src/common/modules/widgets/_widgets/number-card/NumberCard.vue b/apps/web/src/common/modules/widgets/_widgets/number-card/NumberCard.vue index a834b9ec2a..47a53a660f 100644 --- a/apps/web/src/common/modules/widgets/_widgets/number-card/NumberCard.vue +++ b/apps/web/src/common/modules/widgets/_widgets/number-card/NumberCard.vue @@ -221,7 +221,7 @@ const queryResults = useQueries({ ], }); -const widgetLoading = computed(() => queryResults.value?.[0].isPending || queryResults.value?.[0].isFetching); +const widgetLoading = computed(() => queryResults.value?.[0].isFetching); const previousLoading = computed(() => queryResults.value?.[1].isLoading); const errorMessage = computed(() => { if (!state.dataTable) return i18n.t('COMMON.WIDGETS.NO_DATA_TABLE_ERROR_MESSAGE'); diff --git a/apps/web/src/common/modules/widgets/_widgets/pie-chart/PieChart.vue b/apps/web/src/common/modules/widgets/_widgets/pie-chart/PieChart.vue index ce0d2bd368..207206c683 100644 --- a/apps/web/src/common/modules/widgets/_widgets/pie-chart/PieChart.vue +++ b/apps/web/src/common/modules/widgets/_widgets/pie-chart/PieChart.vue @@ -236,7 +236,7 @@ const queryResult = useQuery({ staleTime: WIDGET_LOAD_STALE_TIME, }); -const widgetLoading = computed(() => queryResult.isFetching.value || queryResult.isPending.value); +const widgetLoading = computed(() => queryResult.isFetching.value); const errorMessage = computed(() => { if (!state.dataTable) return i18n.t('COMMON.WIDGETS.NO_DATA_TABLE_ERROR_MESSAGE'); return queryResult.error?.value?.message; diff --git a/apps/web/src/common/modules/widgets/_widgets/sankey-chart/SankeyChart.vue b/apps/web/src/common/modules/widgets/_widgets/sankey-chart/SankeyChart.vue index f01c1563a7..2dbdffbee8 100644 --- a/apps/web/src/common/modules/widgets/_widgets/sankey-chart/SankeyChart.vue +++ b/apps/web/src/common/modules/widgets/_widgets/sankey-chart/SankeyChart.vue @@ -178,7 +178,7 @@ const queryResult = useQuery({ staleTime: WIDGET_LOAD_STALE_TIME, }); -const widgetLoading = computed(() => queryResult.isFetching.value || queryResult.isPending.value); +const widgetLoading = computed(() => queryResult.isFetching.value); const errorMessage = computed(() => { if (!state.dataTable) return i18n.t('COMMON.WIDGETS.NO_DATA_TABLE_ERROR_MESSAGE'); return queryResult.error?.value?.message; diff --git a/apps/web/src/common/modules/widgets/_widgets/stacked-column-chart/StackedColumnChart.vue b/apps/web/src/common/modules/widgets/_widgets/stacked-column-chart/StackedColumnChart.vue index 7fda9247c1..7cbceaf272 100644 --- a/apps/web/src/common/modules/widgets/_widgets/stacked-column-chart/StackedColumnChart.vue +++ b/apps/web/src/common/modules/widgets/_widgets/stacked-column-chart/StackedColumnChart.vue @@ -218,7 +218,7 @@ const queryResult = useQuery({ staleTime: WIDGET_LOAD_STALE_TIME, }); -const widgetLoading = computed(() => queryResult.isFetching.value || queryResult.isPending.value); +const widgetLoading = computed(() => queryResult.isFetching.value); const errorMessage = computed(() => { if (!state.dataTable) return i18n.t('COMMON.WIDGETS.NO_DATA_TABLE_ERROR_MESSAGE'); return queryResult.error?.value?.message; diff --git a/apps/web/src/common/modules/widgets/_widgets/stacked-horizontal-bar-chart/StackedHorizontalBarChart.vue b/apps/web/src/common/modules/widgets/_widgets/stacked-horizontal-bar-chart/StackedHorizontalBarChart.vue index d1161bbf7c..a17c19490c 100644 --- a/apps/web/src/common/modules/widgets/_widgets/stacked-horizontal-bar-chart/StackedHorizontalBarChart.vue +++ b/apps/web/src/common/modules/widgets/_widgets/stacked-horizontal-bar-chart/StackedHorizontalBarChart.vue @@ -212,7 +212,7 @@ const queryResult = useQuery({ staleTime: WIDGET_LOAD_STALE_TIME, }); -const widgetLoading = computed(() => queryResult.isFetching.value || queryResult.isPending.value); +const widgetLoading = computed(() => queryResult.isFetching.value); const errorMessage = computed(() => { if (!state.dataTable) return i18n.t('COMMON.WIDGETS.NO_DATA_TABLE_ERROR_MESSAGE'); return queryResult.error?.value?.message; diff --git a/apps/web/src/common/modules/widgets/_widgets/table/Table.vue b/apps/web/src/common/modules/widgets/_widgets/table/Table.vue index 8c0d8ce544..b4b51400d7 100644 --- a/apps/web/src/common/modules/widgets/_widgets/table/Table.vue +++ b/apps/web/src/common/modules/widgets/_widgets/table/Table.vue @@ -241,7 +241,7 @@ const queryResults = useQueries({ ], }); -const widgetLoading = computed(() => queryResults.value?.[0].isFetching || queryResults.value?.[0].isPending); +const widgetLoading = computed(() => queryResults.value?.[0].isFetching); const errorMessage = computed(() => { if (!state.dataTable) return i18n.t('COMMON.WIDGETS.NO_DATA_TABLE_ERROR_MESSAGE'); return queryResults.value?.[0].error?.message as string; diff --git a/apps/web/src/common/modules/widgets/_widgets/treemap/Treemap.vue b/apps/web/src/common/modules/widgets/_widgets/treemap/Treemap.vue index 5cc64689e4..762e98d215 100644 --- a/apps/web/src/common/modules/widgets/_widgets/treemap/Treemap.vue +++ b/apps/web/src/common/modules/widgets/_widgets/treemap/Treemap.vue @@ -178,7 +178,7 @@ const queryResult = useQuery({ staleTime: WIDGET_LOAD_STALE_TIME, }); -const widgetLoading = computed(() => queryResult.isFetching.value || queryResult.isPending.value); +const widgetLoading = computed(() => queryResult.isFetching.value); const errorMessage = computed(() => { if (!state.dataTable) return i18n.t('COMMON.WIDGETS.NO_DATA_TABLE_ERROR_MESSAGE'); return queryResult.error?.value?.message; 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/apps/web/src/services/asset-inventory-v1/components/MetricExplorerPeriodDropdown.vue b/apps/web/src/services/asset-inventory-v1/components/MetricExplorerPeriodDropdown.vue index 90638638f2..05716e71b5 100644 --- a/apps/web/src/services/asset-inventory-v1/components/MetricExplorerPeriodDropdown.vue +++ b/apps/web/src/services/asset-inventory-v1/components/MetricExplorerPeriodDropdown.vue @@ -2,11 +2,12 @@ import { computed, reactive, watch, } from 'vue'; +import { useRoute } from 'vue-router/composables'; import dayjs from 'dayjs'; -import { isEqual, range } from 'lodash'; +import { isEmpty, isEqual, range } from 'lodash'; -import { PSelectDropdown } from '@cloudforet/mirinae'; +import { PSelectDropdown, PBadge } from '@cloudforet/mirinae'; import type { MenuItem } from '@cloudforet/mirinae/types/controls/context-menu/type'; import type { SelectDropdownMenuItem } from '@cloudforet/mirinae/types/controls/dropdown/select-dropdown/type'; @@ -31,6 +32,8 @@ import type { const metricExplorerPageStore = useMetricExplorerPageStore(); const metricExplorerPageState = metricExplorerPageStore.state; +const route = useRoute(); + const state = reactive({ dailyPeriodMenuItems: computed(() => { const locale = i18n.locale; @@ -57,9 +60,14 @@ const state = reactive({ }, ...(range(12).map((i) => { const start = dayjs.utc().subtract(i, 'month').startOf('month'); + const end = dayjs.utc().subtract(i, 'month').endOf('month'); return { name: start.format('YYYY-MM'), label: dayjs(start).locale(locale).format('MMMM, YYYY'), + period: { + start: start.format('YYYY-MM-DD'), + end: end.format('YYYY-MM-DD'), + }, }; })), ]; @@ -108,6 +116,19 @@ const state = reactive({ }), selectedPeriod: METRIC_PERIOD_MENU.LAST_7_DAYS as MetricPeriodMenu, customDateModalVisible: false, + hasURLQuery: computed(() => !!route.query && !isEmpty(route.query)), + showPeriodBadge: computed(() => state.selectedPeriod === 'custom'), + periodBadgeText: computed(() => { + if (!metricExplorerPageState.period) return ''; + let startDateFormat = 'MMM D'; + if (metricExplorerPageState.granularity === GRANULARITY.MONTHLY) startDateFormat = 'MMM YYYY'; + const endDateFormat = metricExplorerPageState.granularity === GRANULARITY.DAILY ? 'MMM D, YYYY' : startDateFormat; + // + const start = dayjs.utc(metricExplorerPageState.period.start); + let end = dayjs.utc(metricExplorerPageState.period.end); + if (metricExplorerPageState.granularity === GRANULARITY.DAILY) end = dayjs.utc(metricExplorerPageState.period.end).endOf('month'); + return `${start.format(startDateFormat)} ~ ${end.format(endDateFormat)}`; + }), }); /* Util */ @@ -116,8 +137,8 @@ const initSelectedPeriod = () => { if (metricExplorerPageState.relativePeriod) { state.selectedPeriod = getPeriodItemNameByRelativePeriod(metricExplorerPageState.relativePeriod); } else if (metricExplorerPageState.granularity === GRANULARITY.DAILY) { - const selectedPeriodItem = state.dailyPeriodMenuItems.find((item) => isEqual(item.name, metricExplorerPageState.period)); - state.selectedPeriod = selectedPeriodItem?.name; + const selectedPeriodItem = state.dailyPeriodMenuItems.find((item) => isEqual(item?.period, metricExplorerPageState.period)); + state.selectedPeriod = selectedPeriodItem?.name || 'custom'; } else { state.selectedPeriod = 'custom'; } @@ -153,12 +174,12 @@ const handleCustomRangeModalConfirm = (start: string, end: string) => { /* Watcher */ watch(() => metricExplorerPageState.refreshMetricPeriodDropdown, (refresh) => { - if (refresh) initSelectedPeriod(); + if (refresh || state.hasURLQuery) initSelectedPeriod(); }, { immediate: true });