From fec1b9f4a28530ab8b6a597d7f1886a86f8cd1e7 Mon Sep 17 00:00:00 2001 From: Yongtae Park Date: Tue, 19 Aug 2025 13:38:10 +0900 Subject: [PATCH 01/13] feat(favorite): create favorite composables Signed-off-by: samuel.park --- .../composables/use-user-config-api.ts | 2 +- .../favorites/core/use-favorite-list.ts | 48 +++++++++++++++++++ .../core/use-user-config-list-query.ts | 33 +++++++++++++ .../core/use-workspace-favorite-list.ts | 35 ++++++++++++++ 4 files changed, 117 insertions(+), 1 deletion(-) create mode 100644 apps/web/src/common/modules/favorites/core/use-favorite-list.ts create mode 100644 apps/web/src/common/modules/favorites/core/use-user-config-list-query.ts create mode 100644 apps/web/src/common/modules/favorites/core/use-workspace-favorite-list.ts diff --git a/apps/web/src/api-clients/config/user-config/composables/use-user-config-api.ts b/apps/web/src/api-clients/config/user-config/composables/use-user-config-api.ts index 2f25742c73..4e266668a9 100644 --- a/apps/web/src/api-clients/config/user-config/composables/use-user-config-api.ts +++ b/apps/web/src/api-clients/config/user-config/composables/use-user-config-api.ts @@ -16,7 +16,7 @@ export const useUserConfigApi = () => { delete: SpaceConnector.clientV2.config.userConfig.delete, get: >(params: UserConfigGetParameters) => SpaceConnector.clientV2.config.userConfig.get>(params), list: >(params: UserConfigListParameters) => SpaceConnector.clientV2.config.userConfig.list>>(params), - set: SpaceConnector.clientV2.config.userConfig.set, + set: >(params: UserConfigSetParameters) => SpaceConnector.clientV2.config.userConfig.set>(params), }; return { diff --git a/apps/web/src/common/modules/favorites/core/use-favorite-list.ts b/apps/web/src/common/modules/favorites/core/use-favorite-list.ts new file mode 100644 index 0000000000..629e34261d --- /dev/null +++ b/apps/web/src/common/modules/favorites/core/use-favorite-list.ts @@ -0,0 +1,48 @@ +import { computed } from 'vue'; + +import { ApiQueryHelper } from '@cloudforet/core-lib/space-connector/helper'; + +import { useUserWorkspaceStore } from '@/store/app-context/workspace/user-workspace-store'; +import { useUserStore } from '@/store/user/user-store'; + +import { useUserConfigListQuery } from '@/common/modules/favorites/core/use-user-config-list-query'; +import { FAVORITE_TYPE } from '@/common/modules/favorites/favorite-button/type'; + + +const favoriteListApiQuery = new ApiQueryHelper().setSort('updated_at', true); + +export const useFavoriteList = () => { + const userStore = useUserStore(); + const userWorkspaceStore = useUserWorkspaceStore(); + + const userId = computed(() => userStore.state.userId); + const currentWorkspaceId = computed(() => userWorkspaceStore.getters.currentWorkspaceId); + + const { data: favoriteList } = useUserConfigListQuery({ + params: computed(() => { + favoriteListApiQuery.setFilters([ + { k: 'user_id', v: userId.value || '', o: '=' }, + { k: 'name', v: 'console:favorite:', o: '' }, + { k: 'data.workspaceId', v: currentWorkspaceId.value || '', o: '=' }, + ]); + return { + query: favoriteListApiQuery.data, + }; + }), + enabled: computed(() => !!userId.value && !!currentWorkspaceId.value), + }); + + const favoriteMenuList = computed(() => (favoriteList.value?.results ?? []).map((item) => item.data)); + return { + favoriteMenuList, + menuItems: computed(() => favoriteMenuList.value?.filter((item) => item.itemType === FAVORITE_TYPE.MENU)), + projectItems: computed(() => favoriteMenuList.value?.filter((item) => item.itemType === FAVORITE_TYPE.PROJECT)), + projectGroupItems: computed(() => favoriteMenuList.value?.filter((item) => item.itemType === FAVORITE_TYPE.PROJECT_GROUP)), + metricItems: computed(() => favoriteMenuList.value?.filter((item) => item.itemType === FAVORITE_TYPE.METRIC)), + metricExampleItems: computed(() => favoriteMenuList.value?.filter((item) => item.itemType === FAVORITE_TYPE.METRIC_EXAMPLE)), + dashboardItems: computed(() => favoriteMenuList.value?.filter((item) => item.itemType === FAVORITE_TYPE.DASHBOARD)), + costAnalysisItems: computed(() => favoriteMenuList.value?.filter((item) => item.itemType === FAVORITE_TYPE.COST_ANALYSIS)), + securityItems: computed(() => favoriteMenuList.value?.filter((item) => item.itemType === FAVORITE_TYPE.SECURITY)), + serviceItems: computed(() => favoriteMenuList.value?.filter((item) => item.itemType === FAVORITE_TYPE.SERVICE)), + }; +}; diff --git a/apps/web/src/common/modules/favorites/core/use-user-config-list-query.ts b/apps/web/src/common/modules/favorites/core/use-user-config-list-query.ts new file mode 100644 index 0000000000..1fa3d8a57d --- /dev/null +++ b/apps/web/src/common/modules/favorites/core/use-user-config-list-query.ts @@ -0,0 +1,33 @@ +import type { ComputedRef } from 'vue'; +import { computed } from 'vue'; + +import { useUserConfigApi } from '@/api-clients/config/user-config/composables/use-user-config-api'; +import type { UserConfigListParameters } from '@/api-clients/config/user-config/schema/api-verbs/list'; +import { useServiceQueryKey } from '@/query/core/query-key/use-service-query-key'; +import { useScopedQuery } from '@/query/service-query/use-scoped-query'; + +interface UseUserConfigListQueryOptions { + params: ComputedRef; + enabled?: ComputedRef; +} + + + +export const useUserConfigListQuery = ({ params, enabled }: UseUserConfigListQueryOptions) => { + const { userConfigAPI } = useUserConfigApi(); + + const { key, params: queryParams } = useServiceQueryKey('config', 'user-config', 'list', { + params, + }); + + return useScopedQuery({ + queryKey: key, + queryFn: () => userConfigAPI.list(queryParams.value), + enabled: computed(() => { + if (enabled === undefined) { + return true; + } + return enabled.value; + }), + }, ['WORKSPACE', 'USER']); +}; diff --git a/apps/web/src/common/modules/favorites/core/use-workspace-favorite-list.ts b/apps/web/src/common/modules/favorites/core/use-workspace-favorite-list.ts new file mode 100644 index 0000000000..3fb99a7947 --- /dev/null +++ b/apps/web/src/common/modules/favorites/core/use-workspace-favorite-list.ts @@ -0,0 +1,35 @@ +import { computed } from 'vue'; + +import { ApiQueryHelper } from '@cloudforet/core-lib/space-connector/helper'; + +import { useUserStore } from '@/store/user/user-store'; + +import { useUserConfigListQuery } from '@/common/modules/favorites/core/use-user-config-list-query'; +import { FAVORITE_TYPE } from '@/common/modules/favorites/favorite-button/type'; + + +const workspaceFavoriteListApiQuery = new ApiQueryHelper().setSort('updated_at', true); + +export const useWorkspaceFavoriteList = () => { + const userStore = useUserStore(); + + const userId = computed(() => userStore.state.userId); + + const { data: workspaceFavoriteList } = useUserConfigListQuery({ + params: computed(() => { + workspaceFavoriteListApiQuery.setFilters([ + { k: 'user_id', v: userId.value || '', o: '=' }, + { k: 'name', v: `console:favorite:${FAVORITE_TYPE.WORKSPACE}`, o: '' }, + ]); + return { + query: workspaceFavoriteListApiQuery.data, + }; + }), + enabled: computed(() => !!userId.value), + }); + + const workspaceFavoriteMenuList = computed(() => (workspaceFavoriteList.value?.results ?? []).map((item) => item.data)); + return { + workspaceFavoriteMenuList, + }; +}; From a771571a07c98e7cfa6499e5d1d86c1627ee513d Mon Sep 17 00:00:00 2001 From: "samuel.park" Date: Tue, 19 Aug 2025 14:38:26 +0900 Subject: [PATCH 02/13] chore(resource-query-sync): edit wrong queryClient import Signed-off-by: samuel.park --- .../composable/use-resource-cache-sync.ts | 80 +++++++++---------- 1 file changed, 39 insertions(+), 41 deletions(-) diff --git a/apps/web/src/query/resource-query/shared/composable/use-resource-cache-sync.ts b/apps/web/src/query/resource-query/shared/composable/use-resource-cache-sync.ts index 769efcdab4..82eff10290 100644 --- a/apps/web/src/query/resource-query/shared/composable/use-resource-cache-sync.ts +++ b/apps/web/src/query/resource-query/shared/composable/use-resource-cache-sync.ts @@ -1,20 +1,14 @@ -import type { QueryClient } from '@tanstack/vue-query'; -import { useQueryClient } from '@tanstack/vue-query'; - +import { referenceQueryClient as queryClient } from '@/query/clients'; import { useResourceQueryKey } from '@/query/core/query-key/use-resource-query-key'; -import { RESOURCE_CONFIG_MAP } from '@/query/resource-query/shared/contants/resource-config-map'; -import type { ResourceCacheType, ResourceKeyType } from '@/query/resource-query/shared/types/resource-type'; +import type { ResourceKeyType } from '@/query/resource-query/shared/types/resource-type'; -type Obj = Record; type AnyFn = (...args: any[]) => any; type AwaitedRet = Awaited>; export const useResourceCacheSync = (resourceType: ResourceKeyType) => { - const queryClient = useQueryClient(); - const wrapResourceCacheRefresh = (fn: F) => async (...args: Parameters): Promise> => { const { key: referenceQueryKey } = useResourceQueryKey(resourceType); try { @@ -25,40 +19,44 @@ export const useResourceCacheSync = (resourceType: ResourceKeyType) => { } }; - const wrapResourceCacheUpdate = (fn: F) => async (...args: Parameters): Promise> => { - const res = await Promise.resolve(fn(...args)); - await _updateResourceCache(resourceType, res as Obj, queryClient); - return res; - }; + // TODO: annotaion + // const wrapResourceCacheUpdate = (fn: F) => async (...args: Parameters): Promise> => { + // const res = await Promise.resolve(fn(...args)); + // await _updateResourceCache(resourceType, res as Obj, queryClient); + // return res; + // }; - return { wrapResourceCacheRefresh, wrapResourceCacheUpdate }; + return { + wrapResourceCacheRefresh, + // wrapResourceCacheUpdate, + }; }; -const _updateResourceCache = async ( - resourceType: ResourceKeyType, - newData: T, - queryClient: QueryClient, -) => { - const { key: referenceQueryKey, withSuffix } = useResourceQueryKey(resourceType); - const idKey = RESOURCE_CONFIG_MAP[resourceType].idKey as keyof T; - - if (!idKey || typeof newData !== 'object' || newData === null || !(idKey in newData)) { - throw new Error(`Invalid resource key or data for type: ${resourceType}`); - } - - queryClient.setQueryData>(referenceQueryKey.value, (oldData) => { - const currentResults = (oldData ?? {}) as ResourceCacheType; - const newDataId = newData[idKey] as PropertyKey; - - if (newDataId in currentResults) { - return { ...currentResults, [newDataId]: newData }; - } - return { ...currentResults, [newDataId]: newData }; - }); - - await Promise.all([ - queryClient.invalidateQueries({ queryKey: withSuffix('stat') }), - queryClient.invalidateQueries({ queryKey: withSuffix('list') }), - ]); -}; +// const _updateResourceCache = async ( +// resourceType: ResourceKeyType, +// newData: T, +// queryClient: QueryClient, +// ) => { +// const { key: referenceQueryKey, withSuffix } = useResourceQueryKey(resourceType); +// const idKey = RESOURCE_CONFIG_MAP[resourceType].idKey as keyof T; + +// if (!idKey || typeof newData !== 'object' || newData === null || !(idKey in newData)) { +// throw new Error(`Invalid resource key or data for type: ${resourceType}`); +// } + +// queryClient.setQueryData>(referenceQueryKey.value, (oldData) => { +// const currentResults = (oldData ?? {}) as ResourceCacheType; +// const newDataId = newData[idKey] as PropertyKey; + +// if (newDataId in currentResults) { +// return { ...currentResults, [newDataId]: newData }; +// } +// return { ...currentResults, [newDataId]: newData }; +// }); + +// await Promise.all([ +// queryClient.invalidateQueries({ queryKey: withSuffix('stat') }), +// queryClient.invalidateQueries({ queryKey: withSuffix('list') }), +// ]); +// }; From 742ce6b9dbf03a41b84ec67324af55e779488a0b Mon Sep 17 00:00:00 2001 From: Yongtae Park Date: Tue, 19 Aug 2025 21:45:20 +0900 Subject: [PATCH 03/13] feat(config-data): create new config-data composables Signed-off-by: samuel.park --- .../composables/use-user-profile-api.ts | 5 +- .../composables/use-cloud-service-type-map.ts | 38 +++ .../composables/use-cost-query-set-map.ts | 84 ++++++ .../composables/use-dashboard-map.ts | 64 ++++ .../composables/use-metric-example-map.ts | 36 +++ .../composables/use-workspace-map.ts | 31 ++ .../common/composables/config-data/index.ts | 275 ++++++++++++++++++ 7 files changed, 531 insertions(+), 2 deletions(-) create mode 100644 apps/web/src/common/composables/config-data/composables/use-cloud-service-type-map.ts create mode 100644 apps/web/src/common/composables/config-data/composables/use-cost-query-set-map.ts create mode 100644 apps/web/src/common/composables/config-data/composables/use-dashboard-map.ts create mode 100644 apps/web/src/common/composables/config-data/composables/use-metric-example-map.ts create mode 100644 apps/web/src/common/composables/config-data/composables/use-workspace-map.ts create mode 100644 apps/web/src/common/composables/config-data/index.ts diff --git a/apps/web/src/api-clients/identity/user-profile/composables/use-user-profile-api.ts b/apps/web/src/api-clients/identity/user-profile/composables/use-user-profile-api.ts index ca74db5831..4122ee08d0 100644 --- a/apps/web/src/api-clients/identity/user-profile/composables/use-user-profile-api.ts +++ b/apps/web/src/api-clients/identity/user-profile/composables/use-user-profile-api.ts @@ -1,5 +1,6 @@ import { SpaceConnector } from '@cloudforet/core-lib/space-connector'; +import type { ListResponse } from '@/api-clients/_common/schema/api-verbs/list'; import type { UserProfileConfirmEmailParameters } from '@/api-clients/identity/user-profile/schema/api-verbs/confirm-email'; import type { UserProfileConfirmMfaParameters } from '@/api-clients/identity/user-profile/schema/api-verbs/confirm-mfa'; import type { UserProfileDisableMfaParameters } from '@/api-clients/identity/user-profile/schema/api-verbs/disable-mfa'; @@ -10,7 +11,7 @@ import type { UserProfileUpdateParameters } from '@/api-clients/identity/user-pr import type { UserProfileUpdatePasswordParameters } from '@/api-clients/identity/user-profile/schema/api-verbs/update-password'; import type { UserProfileVerifyEmailParameters } from '@/api-clients/identity/user-profile/schema/api-verbs/verify-email'; import type { UserModel } from '@/api-clients/identity/user/schema/model'; - +import type { WorkspaceModel } from '@/api-clients/identity/workspace/schema/model'; export const useUserProfileApi = () => { @@ -23,7 +24,7 @@ export const useUserProfileApi = () => { disableMfa: SpaceConnector.clientV2.identity.userProfile.disableMfa, enableMfa: SpaceConnector.clientV2.identity.userProfile.enableMfa, confirmMfa: SpaceConnector.clientV2.identity.userProfile.confirmMfa, - getWorkspaces: SpaceConnector.clientV2.identity.userProfile.getWorkspaces, + getWorkspaces: SpaceConnector.clientV2.identity.userProfile.getWorkspaces>, getWorkspaceGroups: SpaceConnector.clientV2.identity.userProfile.getWorkspaceGroups, }; diff --git a/apps/web/src/common/composables/config-data/composables/use-cloud-service-type-map.ts b/apps/web/src/common/composables/config-data/composables/use-cloud-service-type-map.ts new file mode 100644 index 0000000000..9ce661b0ac --- /dev/null +++ b/apps/web/src/common/composables/config-data/composables/use-cloud-service-type-map.ts @@ -0,0 +1,38 @@ +import { computed } from 'vue'; + +import { useCloudServiceTypeApi } from '@/api-clients/inventory/cloud-service-type/composables/use-cloud-service-type-api'; +import type { CloudServiceTypeModel } from '@/api-clients/inventory/cloud-service-type/schema/model'; +import { useServiceQueryKey } from '@/query/core/query-key/use-service-query-key'; +import { useScopedQuery } from '@/query/service-query/use-scoped-query'; + +export const useCloudServiceTypeMap = () => { + const { cloudServiceTypeAPI } = useCloudServiceTypeApi(); + + const { key, params } = useServiceQueryKey('inventory', 'cloud-service-type', 'list', { + params: { + query: { + only: ['name', 'group', 'provider', 'cloud_service_type_key', 'tags'], + }, + }, + }); + const { data: cloudServiceTypeList, isFetching: isFetchingCloudServiceTypeList } = useScopedQuery({ + queryKey: key, + queryFn: () => cloudServiceTypeAPI.list(params.value), + select: (data) => data.results || [], + staleTime: 1000 * 60 * 5, + gcTime: 1000 * 60 * 10, + }, ['WORKSPACE']); + + + return { + map: computed(() => { + const cloudServiceTypeMap = new Map(); + cloudServiceTypeList.value?.forEach((d) => { + cloudServiceTypeMap.set(d.cloud_service_type_key, d); + }); + + return cloudServiceTypeMap; + }), + loading: isFetchingCloudServiceTypeList, + }; +}; diff --git a/apps/web/src/common/composables/config-data/composables/use-cost-query-set-map.ts b/apps/web/src/common/composables/config-data/composables/use-cost-query-set-map.ts new file mode 100644 index 0000000000..bdc7e2f4b8 --- /dev/null +++ b/apps/web/src/common/composables/config-data/composables/use-cost-query-set-map.ts @@ -0,0 +1,84 @@ +import { + reactive, watch, readonly, ref, + computed, +} from 'vue'; + +import { useQueryClient } from '@tanstack/vue-query'; + +import type { ListResponse } from '@/api-clients/_common/schema/api-verbs/list'; +import { useCostQuerySetApi } from '@/api-clients/cost-analysis/cost-query-set/composables/use-cost-query-set-api'; +import type { CostQuerySetModel } from '@/api-clients/cost-analysis/cost-query-set/schema/model'; +import { useDataSourceApi } from '@/api-clients/cost-analysis/data-source/composables/use-data-source-api'; +import { useServiceQueryKey } from '@/query/core/query-key/use-service-query-key'; +import { useScopedQuery } from '@/query/service-query/use-scoped-query'; + +export const useCostQuerySetMap = () => { + const queryClient = useQueryClient(); + const { dataSourceAPI } = useDataSourceApi(); + const { costQuerySetAPI } = useCostQuerySetApi(); + const loading = ref(false); + + const { key: dataSourceKey, params: dataSourceParams } = useServiceQueryKey('cost-analysis', 'data-source', 'list', { + params: { + query: { + only: ['data_source_id'], + }, + }, + }); + const { withSuffix } = useServiceQueryKey('cost-analysis', 'cost-query-set', 'list', { + params: { + query: { + only: ['cost_query_set_id', 'name', 'data_source_id'], + }, + }, + }); + + const { data: dataSourceIds, isFetching: isFetchingDataSourceIds } = useScopedQuery({ + queryKey: dataSourceKey, + queryFn: () => dataSourceAPI.list(dataSourceParams.value), + select: (data) => (data.results || []).map((d) => d.data_source_id), + staleTime: 1000 * 60 * 5, + gcTime: 1000 * 60 * 10, + }, ['WORKSPACE']); + + const staticQueryParams = { + query: { + only: ['cost_query_set_id', 'name', 'data_source_id'], + }, + }; + const costQuerySetFetcher = async (dataSourceId: string) => { + const costQuerySetParams = { + data_source_id: dataSourceId, + ...staticQueryParams, + }; + const response = await queryClient.ensureQueryData>({ + queryKey: withSuffix([dataSourceId, costQuerySetParams]), + queryFn: () => costQuerySetAPI.list(costQuerySetParams), + staleTime: 1000 * 60 * 5, + gcTime: 1000 * 60 * 10, + }); + + return response.results || []; + }; + + const costQuerySetMap = reactive>({}); + + + watch(dataSourceIds, async () => { + if (!dataSourceIds.value?.length) return; + loading.value = true; + const promises = dataSourceIds.value.map((dataSourceId) => costQuerySetFetcher(dataSourceId)); + const results = await Promise.all(promises); + results.forEach((data) => { + data.forEach((d) => { + costQuerySetMap[d.cost_query_set_id] = d; + }); + }); + loading.value = false; + }); + + return { + map: readonly(costQuerySetMap), + loading: computed(() => loading.value || isFetchingDataSourceIds.value), + }; +}; diff --git a/apps/web/src/common/composables/config-data/composables/use-dashboard-map.ts b/apps/web/src/common/composables/config-data/composables/use-dashboard-map.ts new file mode 100644 index 0000000000..2c41b68d7a --- /dev/null +++ b/apps/web/src/common/composables/config-data/composables/use-dashboard-map.ts @@ -0,0 +1,64 @@ +import { computed } from 'vue'; + +import { ApiQueryHelper } from '@cloudforet/core-lib/space-connector/helper'; + +import type { DashboardModel } from '@/api-clients/dashboard/_types/dashboard-type'; +import { usePrivateDashboardApi } from '@/api-clients/dashboard/private-dashboard/composables/use-private-dashboard-api'; +import { usePublicDashboardApi } from '@/api-clients/dashboard/public-dashboard/composables/use-public-dashboard-api'; +import { useServiceQueryKey } from '@/query/core/query-key/use-service-query-key'; +import { useScopedQuery } from '@/query/service-query/use-scoped-query'; + +const privateDashboardListApiQueryHelper = new ApiQueryHelper().setOnly('dashboard_id', 'name'); +const publicDashboardListApiQueryHelper = new ApiQueryHelper().setFilters([ + { k: 'resource_group', v: 'WORKSPACE', o: '=' }, +]).setOnly('dashboard_id', 'name'); + + +export const useDashboardMap = () => { + const { publicDashboardAPI } = usePublicDashboardApi(); + const { privateDashboardAPI } = usePrivateDashboardApi(); + + const { key: publicDashboardKey, params: publicDashboardParams } = useServiceQueryKey('dashboard', 'public-dashboard', 'list', { + params: { + query: publicDashboardListApiQueryHelper.data, + }, + }); + + const { key: privateDashboardKey, params: privateDashboardParams } = useServiceQueryKey('dashboard', 'private-dashboard', 'list', { + params: { + query: privateDashboardListApiQueryHelper.data, + }, + }); + const { data: publicDashboardList, isFetching: isFetchingPublicDashboardList } = useScopedQuery({ + queryKey: publicDashboardKey, + queryFn: () => publicDashboardAPI.list(publicDashboardParams.value), + select: (data) => data.results || [], + staleTime: 1000 * 60 * 5, + gcTime: 1000 * 60 * 10, + }, ['WORKSPACE']); + + const { data: privateDashboardList, isFetching: isFetchingPrivateDashboardList } = useScopedQuery({ + queryKey: privateDashboardKey, + queryFn: () => privateDashboardAPI.list(privateDashboardParams.value), + select: (data) => data.results || [], + staleTime: 1000 * 60 * 5, + gcTime: 1000 * 60 * 10, + }, ['WORKSPACE']); + + + return { + map: computed(() => { + const dashboardMap = new Map(); + publicDashboardList.value?.forEach((d) => { + dashboardMap.set(d.dashboard_id, d); + }); + + privateDashboardList.value?.forEach((d) => { + dashboardMap.set(d.dashboard_id, d); + }); + + return dashboardMap; + }), + loading: computed(() => isFetchingPublicDashboardList.value || isFetchingPrivateDashboardList.value), + }; +}; diff --git a/apps/web/src/common/composables/config-data/composables/use-metric-example-map.ts b/apps/web/src/common/composables/config-data/composables/use-metric-example-map.ts new file mode 100644 index 0000000000..120511a9aa --- /dev/null +++ b/apps/web/src/common/composables/config-data/composables/use-metric-example-map.ts @@ -0,0 +1,36 @@ +import { computed } from 'vue'; + +import { useMetricExampleApi } from '@/api-clients/inventory/metric-example/composables/use-metric-example-api'; +import type { MetricExampleModel } from '@/api-clients/inventory/metric-example/schema/model'; +import { useServiceQueryKey } from '@/query/core/query-key/use-service-query-key'; +import { useScopedQuery } from '@/query/service-query/use-scoped-query'; + +export const useMetricExampleMap = () => { + const { metricExampleAPI } = useMetricExampleApi(); + + const { key, params: queryParams } = useServiceQueryKey('inventory', 'metric-example', 'list', { + params: { + query: { + only: ['example_id', 'name'], + }, + }, + }); + const { data: metricExampleList, isFetching: isFetchingMetricExampleList } = useScopedQuery({ + queryKey: key, + queryFn: () => metricExampleAPI.list(queryParams.value), + select: (data) => data.results ?? [], + staleTime: 1000 * 60 * 5, + gcTime: 1000 * 60 * 10, + }, ['WORKSPACE']); + + return { + map: computed>(() => { + const metricExampleMap = new Map(); + metricExampleList.value?.forEach((item) => { + metricExampleMap.set(item.example_id, item); + }); + return metricExampleMap; + }), + loading: isFetchingMetricExampleList, + }; +}; diff --git a/apps/web/src/common/composables/config-data/composables/use-workspace-map.ts b/apps/web/src/common/composables/config-data/composables/use-workspace-map.ts new file mode 100644 index 0000000000..de3d670fc5 --- /dev/null +++ b/apps/web/src/common/composables/config-data/composables/use-workspace-map.ts @@ -0,0 +1,31 @@ +import { computed } from 'vue'; + +import { useUserProfileApi } from '@/api-clients/identity/user-profile/composables/use-user-profile-api'; +import type { WorkspaceModel } from '@/api-clients/identity/workspace/schema/model'; +import { useServiceQueryKey } from '@/query/core/query-key/use-service-query-key'; +import { useScopedQuery } from '@/query/service-query/use-scoped-query'; + +export const useWorkspaceMap = () => { + const { userProfileAPI } = useUserProfileApi(); + + const { key, params } = useServiceQueryKey('identity', 'user-profile', 'get-workspaces'); + const { data: workspaceList, isFetching: isFetchingWorkspaceList } = useScopedQuery({ + queryKey: key, + queryFn: () => userProfileAPI.getWorkspaces(params.value), + select: (data) => data.results || [], + staleTime: 1000 * 60 * 5, + gcTime: 1000 * 60 * 10, + }, ['WORKSPACE', 'USER']); + + + return { + map: computed(() => { + const workspaceMap = new Map(); + workspaceList.value?.forEach((workspace) => { + workspaceMap.set(workspace.workspace_id, workspace); + }); + return workspaceMap; + }), + loading: isFetchingWorkspaceList, + }; +}; diff --git a/apps/web/src/common/composables/config-data/index.ts b/apps/web/src/common/composables/config-data/index.ts new file mode 100644 index 0000000000..c7e2dc0dbd --- /dev/null +++ b/apps/web/src/common/composables/config-data/index.ts @@ -0,0 +1,275 @@ +import type { ComputedRef } from 'vue'; +import { computed } from 'vue'; +import { useRoute, useRouter } from 'vue-router/composables'; + +import { cloneDeep, find } from 'lodash'; + +import { useAllReferenceDataModel } from '@/query/resource-query/reference-data-model'; + +import type { DisplayMenu } from '@/store/menu/type'; + +import { assetUrlConverter } from '@/lib/helper/asset-helper'; +import { getParsedKeysWithManagedCostQueryFavoriteKey } from '@/lib/helper/config-data-helper'; +import { getAllSuggestionMenuList } from '@/lib/helper/menu-suggestion-helper'; +import { useAllMenuList } from '@/lib/menu/use-all-menu-list'; + + +import { useCloudServiceTypeMap } from '@/common/composables/config-data/composables/use-cloud-service-type-map'; +import { useCostQuerySetMap } from '@/common/composables/config-data/composables/use-cost-query-set-map'; +import { useDashboardMap } from '@/common/composables/config-data/composables/use-dashboard-map'; +import { useMetricExampleMap } from '@/common/composables/config-data/composables/use-metric-example-map'; +import { useWorkspaceMap } from '@/common/composables/config-data/composables/use-workspace-map'; +import { + FAVORITE_TYPE, type FavoriteConfig, type FavoriteType, type FavoriteItem, +} from '@/common/modules/favorites/favorite-button/type'; +import type { RecentConfig, RecentType } from '@/common/modules/navigations/type'; + +export interface ConfigData extends Omit, Omit { + itemType: RecentType|FavoriteType; +} + +export interface ReferenceData extends Omit { + itemType: RecentType|FavoriteType; +} + +interface UseConvertReferencedConfigDataOptions { + allConfigList?: ComputedRef; + projectConfigList?: ComputedRef; + projectGroupConfigList?: ComputedRef; + metricConfigList?: ComputedRef; + metricExampleConfigList?: ComputedRef; + serviceConfigList?: ComputedRef; + dashboardConfigList?: ComputedRef; + costQuerySetConfigList?: ComputedRef; + cloudServiceConfigList?: ComputedRef; + workspaceConfigList?: ComputedRef; +} + +export const useConvertReferencedConfigData = ({ + allConfigList, + projectConfigList, + projectGroupConfigList, + metricConfigList, + metricExampleConfigList, + serviceConfigList, + dashboardConfigList, + costQuerySetConfigList, + cloudServiceConfigList, + workspaceConfigList, +}: UseConvertReferencedConfigDataOptions) => { + /* Reference */ + const referenceMap = useAllReferenceDataModel(); + const projectMap = referenceMap.project; + const projectGroupMap = referenceMap.projectGroup; + const metricMap = referenceMap.metric; + const serviceMap = referenceMap.service; + const costDataSourceMap = referenceMap.costDataSource; + const { map: cloudServiceTypeMap, loading: isLoadingCloudServiceTypeMap } = useCloudServiceTypeMap(); + const { map: dashboardMap, loading: isLoadingDashboardMap } = useDashboardMap(); + const { map: costQuerySetMap, loading: isLoadingCostQuerySetMap } = useCostQuerySetMap(); + const { map: metricExampleMap, loading: isLoadingMetricExampleMap } = useMetricExampleMap(); + const { map: workspaceMap, loading: isLoadingWorkspaceMap } = useWorkspaceMap(); + + /* Menu */ + const { getAllMenuList } = useAllMenuList(); + const router = useRouter(); + const route = useRoute(); + const allMenuList = computed(() => getAllMenuList(route, router)); + + /* Converted */ + const convertedMenuList = computed(() => { + const convertMenuList = cloneDeep(allMenuList.value); + + const suggestionMenuList = getAllSuggestionMenuList(convertMenuList); + const results: ReferenceData[] = []; + if (!allConfigList?.value) return results; + + const reorderedConfig: ConfigData[] = []; + suggestionMenuList.forEach((menu) => { + const configItem = allConfigList?.value.find((item) => item.itemId === menu.id); + if (configItem) { + reorderedConfig.push(configItem); + } + }); + + return reorderedConfig.map((d) => { + const menu = find(suggestionMenuList, { id: d.itemId }); + return { + ...d, + itemType: FAVORITE_TYPE.MENU, + itemId: menu?.id || d.itemId, + name: menu?.id || d.itemId, + label: menu?.label || d.itemId, + icon: menu?.parents?.[0]?.icon || menu?.icon || undefined, + parents: menu?.parents || [], + updatedAt: d?.updatedAt, + isDeleted: !menu, + }; + }); + }); + + const convertedProjectList = computed(() => (projectConfigList?.value ?? []).map((d) => { + const project = projectMap[d.itemId]; + const parents = project?.data?.groupInfo?.id ? [{ + name: project.data?.groupInfo?.id, + label: project.data?.groupInfo?.name, + }] : undefined; + return { + ...d, + name: d.itemId, + label: project?.name || d.itemId, + icon: 'ic_document-filled', + updatedAt: d?.updatedAt, + parents, + isDeleted: !project, + }; + })); + + const convertedProjectGroupList = computed(() => (projectGroupConfigList?.value ?? []).map((d) => { + const projectGroup = projectGroupMap[d.itemId]; + const parents = projectGroup?.data?.parentGroupInfo?.id ? [{ + name: projectGroup.data?.parentGroupInfo?.id, + label: projectGroup.data?.parentGroupInfo?.name, + }] : undefined; + return { + ...d, + name: d.itemId, + label: projectGroup?.name || d.itemId, + icon: 'ic_folder-filled', + parents, + isDeleted: !projectGroup, + }; + })); + + const convertedCloudServiceTypeList = computed(() => (cloudServiceConfigList?.value ?? []).map((d) => { + const cloudServiceType = cloudServiceTypeMap.value.get(d.itemId); + return { + ...d, + name: d.itemId, + label: cloudServiceType?.name || d.itemId, + icon: assetUrlConverter(cloudServiceType?.tags['spaceone:icon']), + provider: cloudServiceType?.provider, + parents: [{ + name: cloudServiceType?.group, + label: cloudServiceType?.group, + }], + updatedAt: d.updatedAt, + isDeleted: !cloudServiceType, + }; + })); + + const convertedMetricList = computed(() => (metricConfigList?.value ?? []).map((d) => { + const metric = metricMap[d.itemId]; + return { + ...d, + name: d.itemId, + label: metric?.name || d.itemId, + icon: d.itemId.startsWith('metric-managed-') ? 'ic_main-filled' : 'ic_sub', + updatedAt: d?.updatedAt, + isDeleted: !metric, + }; + })); + + const convertedMetricExampleList = computed(() => (metricExampleConfigList?.value ?? []).map((d) => { + const metricExample = metricExampleMap.value.get(d.itemId); + return { + ...d, + name: d.itemId, + label: metricExample?.name || d.itemId, + icon: 'ic_example-filled', + updatedAt: d?.updatedAt, + isDeleted: !metricExample, + }; + })); + + const convertedServiceList = computed(() => (serviceConfigList?.value ?? []).map((d) => { + const service = serviceMap[d.itemId]; + return { + ...d, + itemType: FAVORITE_TYPE.SERVICE, + itemId: service?.name || d.itemId, + name: service?.name || d.itemId, + label: service?.label || d.itemId, + icon: 'ic_service_alert', + updatedAt: d?.updatedAt, + isDeleted: !service, + }; + })); + + const convertedDashboardList = computed(() => (dashboardConfigList?.value ?? []).map((d) => { + const dashboard = dashboardMap.value.get(d.itemId); + return { + ...d, + name: dashboard?.dashboard_id || d.itemId, + label: dashboard?.name || d.itemId, + icon: 'ic_service_dashboard', + updatedAt: d.updatedAt, + isDeleted: !dashboard, + }; + })); + + const convertedCostQuerySetList = computed(() => (costQuerySetConfigList?.value ?? []).map((d) => { + const parsedKeys = getParsedKeysWithManagedCostQueryFavoriteKey(d.itemId); + if (parsedKeys) { + const [dataSourceId, costQuerySetId] = parsedKeys; + const dataSource = costDataSourceMap[dataSourceId]; + return { + ...d, + name: d.itemId, + label: costQuerySetId || d.itemId, + updatedAt: d.updatedAt, + icon: 'ic_service_cost-explorer', + dataSourceId, + parents: [{ + name: dataSourceId, + label: dataSource?.label, + }], + isDeleted: false, + }; + } + const costQuerySet = costQuerySetMap[d.itemId]; + return { + ...d, + name: costQuerySet?.cost_query_set_id || d.itemId, + label: costQuerySet?.name || d.itemId, + updatedAt: d.updatedAt, + icon: 'ic_service_cost-explorer', + dataSourceId: costQuerySet?.data_source_id, + parents: [{ + name: costQuerySet?.data_source_id, + label: costDataSourceMap[costQuerySet?.data_source_id]?.label, + }], + isDeleted: !costQuerySet, + }; + })); + + const convertedWorkspaceList = computed(() => (workspaceConfigList?.value ?? []).map((d) => { + const workspace = workspaceMap.value.get(d.itemId); + return { + ...d, + itemType: FAVORITE_TYPE.WORKSPACE, + itemId: workspace?.workspace_id || d.itemId, + name: workspace?.workspace_id || d.itemId, + label: workspace?.name || d.itemId, + tags: workspace?.tags, + isDeleted: !workspace, + }; + })); + + + return { + convertedMenu: convertedMenuList, + convertedProject: convertedProjectList, + convertedProjectGroup: convertedProjectGroupList, + convertedMetric: convertedMetricList, + convertedMetricExample: convertedMetricExampleList, + convertedService: convertedServiceList, + convertedCloudServiceType: convertedCloudServiceTypeList, + convertedDashboard: convertedDashboardList, + convertedCostQuerySet: convertedCostQuerySetList, + convertedWorkspace: convertedWorkspaceList, + loading: computed(() => isLoadingCloudServiceTypeMap.value || isLoadingDashboardMap.value || isLoadingCostQuerySetMap.value || isLoadingMetricExampleMap.value || isLoadingWorkspaceMap.value), + }; +}; + + From 825abedd082ce5779738f9534ba3765f60a3b227 Mon Sep 17 00:00:00 2001 From: Yongtae Park Date: Tue, 19 Aug 2025 21:46:02 +0900 Subject: [PATCH 04/13] feat(favorite): create new favorite composables Signed-off-by: samuel.park --- ...y.ts => use-favorite-config-data-query.ts} | 11 ++++-- .../core/use-favorite-create-mutation.ts | 37 +++++++++++++++++++ .../core/use-favorite-delete-mutation.ts | 33 +++++++++++++++++ .../favorites/core/use-favorite-list.ts | 10 +++-- .../core/use-workspace-favorite-list.ts | 8 ++-- 5 files changed, 90 insertions(+), 9 deletions(-) rename apps/web/src/common/modules/favorites/core/{use-user-config-list-query.ts => use-favorite-config-data-query.ts} (68%) create mode 100644 apps/web/src/common/modules/favorites/core/use-favorite-create-mutation.ts create mode 100644 apps/web/src/common/modules/favorites/core/use-favorite-delete-mutation.ts diff --git a/apps/web/src/common/modules/favorites/core/use-user-config-list-query.ts b/apps/web/src/common/modules/favorites/core/use-favorite-config-data-query.ts similarity index 68% rename from apps/web/src/common/modules/favorites/core/use-user-config-list-query.ts rename to apps/web/src/common/modules/favorites/core/use-favorite-config-data-query.ts index 1fa3d8a57d..7fbcd1acfd 100644 --- a/apps/web/src/common/modules/favorites/core/use-user-config-list-query.ts +++ b/apps/web/src/common/modules/favorites/core/use-favorite-config-data-query.ts @@ -6,23 +6,28 @@ import type { UserConfigListParameters } from '@/api-clients/config/user-config/ import { useServiceQueryKey } from '@/query/core/query-key/use-service-query-key'; import { useScopedQuery } from '@/query/service-query/use-scoped-query'; -interface UseUserConfigListQueryOptions { +import type { ConfigData } from '@/common/composables/config-data'; + + +interface UseFavoriteConfigDataQueryOptions { + configDataType: 'console:favorite' | 'console:favorite:WORKSPACE'; params: ComputedRef; enabled?: ComputedRef; } -export const useUserConfigListQuery = ({ params, enabled }: UseUserConfigListQueryOptions) => { +export const useFavoriteConfigDataQuery = ({ configDataType = 'console:favorite', params, enabled }: UseFavoriteConfigDataQueryOptions) => { const { userConfigAPI } = useUserConfigApi(); const { key, params: queryParams } = useServiceQueryKey('config', 'user-config', 'list', { + contextKey: configDataType, params, }); return useScopedQuery({ queryKey: key, - queryFn: () => userConfigAPI.list(queryParams.value), + queryFn: () => userConfigAPI.list(queryParams.value), enabled: computed(() => { if (enabled === undefined) { return true; diff --git a/apps/web/src/common/modules/favorites/core/use-favorite-create-mutation.ts b/apps/web/src/common/modules/favorites/core/use-favorite-create-mutation.ts new file mode 100644 index 0000000000..4ddb1fe701 --- /dev/null +++ b/apps/web/src/common/modules/favorites/core/use-favorite-create-mutation.ts @@ -0,0 +1,37 @@ +import { useMutation, useQueryClient } from '@tanstack/vue-query'; + +import { useUserConfigApi } from '@/api-clients/config/user-config/composables/use-user-config-api'; +import { useServiceQueryKey } from '@/query/core/query-key/use-service-query-key'; + +import type { ConfigData, ReferenceData } from '@/common/composables/config-data'; +import ErrorHandler from '@/common/composables/error/errorHandler'; +import { FAVORITE_TYPE } from '@/common/modules/favorites/favorite-button/type'; + +export const useFavoriteCreateMutation = () => { + const queryClient = useQueryClient(); + const { userConfigAPI } = useUserConfigApi(); + const { withSuffix } = useServiceQueryKey('config', 'user-config', 'list'); + + return useMutation({ + mutationFn: (params: ConfigData|ReferenceData) => { + const { itemType, workspaceId, itemId } = params; + + return userConfigAPI.set({ + name: itemType === FAVORITE_TYPE.WORKSPACE + ? `console:favorite:${itemType}:${itemId}` + : `console:favorite:${itemType}:${workspaceId}:${itemId}`, + data: { + ...params, + type: 'item', + }, + }); + }, + onSuccess: (_, variables) => { + const configDataType = variables.itemType === FAVORITE_TYPE.WORKSPACE ? 'console:favorite:WORKSPACE' : 'console:favorite'; + queryClient.invalidateQueries({ queryKey: withSuffix(configDataType) }); + }, + onError: (error) => { + ErrorHandler.handleError(error); + }, + }); +}; diff --git a/apps/web/src/common/modules/favorites/core/use-favorite-delete-mutation.ts b/apps/web/src/common/modules/favorites/core/use-favorite-delete-mutation.ts new file mode 100644 index 0000000000..1454771fa0 --- /dev/null +++ b/apps/web/src/common/modules/favorites/core/use-favorite-delete-mutation.ts @@ -0,0 +1,33 @@ +import { useMutation, useQueryClient } from '@tanstack/vue-query'; + +import { useUserConfigApi } from '@/api-clients/config/user-config/composables/use-user-config-api'; +import { useServiceQueryKey } from '@/query/core/query-key/use-service-query-key'; + +import type { ConfigData, ReferenceData } from '@/common/composables/config-data'; +import ErrorHandler from '@/common/composables/error/errorHandler'; +import { FAVORITE_TYPE } from '@/common/modules/favorites/favorite-button/type'; + +export const useFavoriteDeleteMutation = () => { + const queryClient = useQueryClient(); + const { userConfigAPI } = useUserConfigApi(); + const { withSuffix } = useServiceQueryKey('config', 'user-config', 'list'); + + return useMutation({ + mutationFn: (params: ConfigData|ReferenceData) => { + const { itemType, workspaceId, itemId } = params; + + return userConfigAPI.delete({ + name: itemType === FAVORITE_TYPE.WORKSPACE + ? `console:favorite:${itemType}:${itemId}` + : `console:favorite:${itemType}:${workspaceId}:${itemId}`, + }); + }, + onSuccess: (_, variables) => { + const configDataType = variables.itemType === FAVORITE_TYPE.WORKSPACE ? 'console:favorite:WORKSPACE' : 'console:favorite'; + queryClient.invalidateQueries({ queryKey: withSuffix(configDataType) }); + }, + onError: (error) => { + ErrorHandler.handleError(error); + }, + }); +}; diff --git a/apps/web/src/common/modules/favorites/core/use-favorite-list.ts b/apps/web/src/common/modules/favorites/core/use-favorite-list.ts index 629e34261d..9f5d84062a 100644 --- a/apps/web/src/common/modules/favorites/core/use-favorite-list.ts +++ b/apps/web/src/common/modules/favorites/core/use-favorite-list.ts @@ -5,7 +5,8 @@ import { ApiQueryHelper } from '@cloudforet/core-lib/space-connector/helper'; import { useUserWorkspaceStore } from '@/store/app-context/workspace/user-workspace-store'; import { useUserStore } from '@/store/user/user-store'; -import { useUserConfigListQuery } from '@/common/modules/favorites/core/use-user-config-list-query'; +import type { ConfigData } from '@/common/composables/config-data'; +import { useFavoriteConfigDataQuery } from '@/common/modules/favorites/core/use-favorite-config-data-query'; import { FAVORITE_TYPE } from '@/common/modules/favorites/favorite-button/type'; @@ -18,7 +19,8 @@ export const useFavoriteList = () => { const userId = computed(() => userStore.state.userId); const currentWorkspaceId = computed(() => userWorkspaceStore.getters.currentWorkspaceId); - const { data: favoriteList } = useUserConfigListQuery({ + const { data: favoriteList, isFetching: isFetchingFavoriteList } = useFavoriteConfigDataQuery({ + configDataType: 'console:favorite', params: computed(() => { favoriteListApiQuery.setFilters([ { k: 'user_id', v: userId.value || '', o: '=' }, @@ -32,8 +34,9 @@ export const useFavoriteList = () => { enabled: computed(() => !!userId.value && !!currentWorkspaceId.value), }); - const favoriteMenuList = computed(() => (favoriteList.value?.results ?? []).map((item) => item.data)); + const favoriteMenuList = computed(() => (favoriteList.value?.results ?? []).map((item) => item.data)); return { + loading: isFetchingFavoriteList, favoriteMenuList, menuItems: computed(() => favoriteMenuList.value?.filter((item) => item.itemType === FAVORITE_TYPE.MENU)), projectItems: computed(() => favoriteMenuList.value?.filter((item) => item.itemType === FAVORITE_TYPE.PROJECT)), @@ -44,5 +47,6 @@ export const useFavoriteList = () => { costAnalysisItems: computed(() => favoriteMenuList.value?.filter((item) => item.itemType === FAVORITE_TYPE.COST_ANALYSIS)), securityItems: computed(() => favoriteMenuList.value?.filter((item) => item.itemType === FAVORITE_TYPE.SECURITY)), serviceItems: computed(() => favoriteMenuList.value?.filter((item) => item.itemType === FAVORITE_TYPE.SERVICE)), + cloudServiceTypeItems: computed(() => favoriteMenuList.value?.filter((item) => item.itemType === FAVORITE_TYPE.CLOUD_SERVICE_TYPE)), }; }; diff --git a/apps/web/src/common/modules/favorites/core/use-workspace-favorite-list.ts b/apps/web/src/common/modules/favorites/core/use-workspace-favorite-list.ts index 3fb99a7947..1b8210ed8f 100644 --- a/apps/web/src/common/modules/favorites/core/use-workspace-favorite-list.ts +++ b/apps/web/src/common/modules/favorites/core/use-workspace-favorite-list.ts @@ -4,7 +4,7 @@ import { ApiQueryHelper } from '@cloudforet/core-lib/space-connector/helper'; import { useUserStore } from '@/store/user/user-store'; -import { useUserConfigListQuery } from '@/common/modules/favorites/core/use-user-config-list-query'; +import { useFavoriteConfigDataQuery } from '@/common/modules/favorites/core/use-favorite-config-data-query'; import { FAVORITE_TYPE } from '@/common/modules/favorites/favorite-button/type'; @@ -15,7 +15,8 @@ export const useWorkspaceFavoriteList = () => { const userId = computed(() => userStore.state.userId); - const { data: workspaceFavoriteList } = useUserConfigListQuery({ + const { data: workspaceFavoriteList, isFetching: isFetchingWorkspaceFavoriteList } = useFavoriteConfigDataQuery({ + configDataType: 'console:favorite:WORKSPACE', params: computed(() => { workspaceFavoriteListApiQuery.setFilters([ { k: 'user_id', v: userId.value || '', o: '=' }, @@ -30,6 +31,7 @@ export const useWorkspaceFavoriteList = () => { const workspaceFavoriteMenuList = computed(() => (workspaceFavoriteList.value?.results ?? []).map((item) => item.data)); return { - workspaceFavoriteMenuList, + loading: isFetchingWorkspaceFavoriteList, + workspaceItems: workspaceFavoriteMenuList, }; }; From 8190a57d7de50edd635c167f782a3094facde6fb Mon Sep 17 00:00:00 2001 From: Yongtae Park Date: Tue, 19 Aug 2025 21:48:06 +0900 Subject: [PATCH 05/13] feat(favorite): apply new favorite module Signed-off-by: samuel.park --- .../favorite-button/FavoriteButton.vue | 119 +++++++----------- .../modules/navigations/gnb/GNBToolbox.vue | 3 - .../modules/TopBarFavoriteContextMenu.vue | 116 ++++++----------- .../use-service-reference-data-model.ts | 2 +- .../LandingSearchedWorkspaces.vue | 11 +- .../LandingGroupWorkspaces.vue | 15 ++- .../console-translation-2.8.babel | 46 ++++++- packages/language-pack/en.json | 5 +- packages/language-pack/ja.json | 5 +- packages/language-pack/ko.json | 5 +- 10 files changed, 154 insertions(+), 173 deletions(-) diff --git a/apps/web/src/common/modules/favorites/favorite-button/FavoriteButton.vue b/apps/web/src/common/modules/favorites/favorite-button/FavoriteButton.vue index 24f50751af..55e318a358 100644 --- a/apps/web/src/common/modules/favorites/favorite-button/FavoriteButton.vue +++ b/apps/web/src/common/modules/favorites/favorite-button/FavoriteButton.vue @@ -2,44 +2,22 @@ import { computed, reactive, } from 'vue'; -import { useRoute, useRouter } from 'vue-router/composables'; import { PI } from '@cloudforet/mirinae'; -import type { CostQuerySetModel } from '@/api-clients/cost-analysis/cost-query-set/schema/model'; -import type { WorkspaceModel } from '@/api-clients/identity/workspace/schema/model'; -import type { MetricExampleModel } from '@/api-clients/inventory/metric-example/schema/model'; - import { useAppContextStore } from '@/store/app-context/app-context-store'; import { useUserWorkspaceStore } from '@/store/app-context/workspace/user-workspace-store'; -import { useAllReferenceStore } from '@/store/reference/all-reference-store'; -import type { CloudServiceTypeReferenceMap } from '@/store/reference/cloud-service-type-reference-store'; -import type { CostDataSourceReferenceMap } from '@/store/reference/cost-data-source-reference-store'; -import type { MetricReferenceMap } from '@/store/reference/metric-reference-store'; -import type { ProjectGroupReferenceMap } from '@/store/reference/project-group-reference-store'; -import type { ProjectReferenceMap } from '@/store/reference/project-reference-store'; -import type { ServiceReferenceMap } from '@/store/reference/service-reference-store'; import type { ReferenceData } from '@/lib/helper/config-data-helper'; -import { - convertCloudServiceConfigToReferenceData, - convertCostAnalysisConfigToReferenceData, - convertDashboardConfigToReferenceData, - convertMenuConfigToReferenceData, - convertMetricConfigToReferenceData, - convertMetricExampleConfigToReferenceData, - convertProjectConfigToReferenceData, - convertProjectGroupConfigToReferenceData, - convertWorkspaceConfigToReferenceData, - convertServiceConfigToReferenceData, -} from '@/lib/helper/config-data-helper'; -import { useAllMenuList } from '@/lib/menu/use-all-menu-list'; - -import { useGlobalDashboardQuery } from '@/common/composables/global-dashboard/use-global-dashboard-query'; -import { useFavoriteStore } from '@/common/modules/favorites/favorite-button/store/favorite-store'; + +import { useConvertReferencedConfigData } from '@/common/composables/config-data'; +import { useFavoriteCreateMutation } from '@/common/modules/favorites/core/use-favorite-create-mutation'; +import { useFavoriteDeleteMutation } from '@/common/modules/favorites/core/use-favorite-delete-mutation'; +import { useFavoriteList } from '@/common/modules/favorites/core/use-favorite-list'; +import { useWorkspaceFavoriteList } from '@/common/modules/favorites/core/use-workspace-favorite-list'; import type { FavoriteType, FavoriteConfig } from '@/common/modules/favorites/favorite-button/type'; import { FAVORITE_TYPE } from '@/common/modules/favorites/favorite-button/type'; -import { useGnbStore } from '@/common/modules/navigations/stores/gnb-store'; + interface Props { @@ -49,67 +27,67 @@ interface Props { readOnly?: boolean; visibleActiveCaseOnly?: boolean; } +interface Emits { + (e: 'click-favorite'): void; +} const props = withDefaults(defineProps(), { scale: '1', readOnly: false, visibleActiveCaseOnly: false, }); +const emit = defineEmits(); -const route = useRoute(); -const router = useRouter(); - -const allReferenceStore = useAllReferenceStore(); const userWorkspaceStore = useUserWorkspaceStore(); const workspaceStoreGetters = userWorkspaceStore.getters; const appContextStore = useAppContextStore(); -const favoriteStore = useFavoriteStore(); -const favoriteStoreGetters = favoriteStore.getters; -const gnbStore = useGnbStore(); -const gnbStoreGetters = gnbStore.getters; -const { getAllMenuList } = useAllMenuList(); + +/* Favorite */ +const favoriteData = useFavoriteList(); +const { loading: isLoadingWorkspaceFavoriteData, ...workspaceFavoriteData } = useWorkspaceFavoriteList(); +const { loading: isLoadingConvertedFavoriteData, ...convertedFavoriteData } = useConvertReferencedConfigData({ + allConfigList: favoriteData.favoriteMenuList, + projectConfigList: favoriteData.projectItems, + projectGroupConfigList: favoriteData.projectGroupItems, + metricConfigList: favoriteData.metricItems, + metricExampleConfigList: favoriteData.metricExampleItems, + serviceConfigList: favoriteData.serviceItems, + dashboardConfigList: favoriteData.dashboardItems, + costQuerySetConfigList: favoriteData.costAnalysisItems, + cloudServiceConfigList: favoriteData.cloudServiceTypeItems, + workspaceConfigList: workspaceFavoriteData.workspaceItems, +}); + +const { mutateAsync: createFavorite, isPending: isCreatingFavorite } = useFavoriteCreateMutation(); +const { mutateAsync: deleteFavorite, isPending: isDeletingFavorite } = useFavoriteDeleteMutation(); -/* Query */ -const { - publicDashboardListQuery, - privateDashboardListQuery, -} = useGlobalDashboardQuery(); -const emit = defineEmits<{(e: 'click-favorite'): void; -}>(); -const dashboardList = computed(() => [...(publicDashboardListQuery?.data?.value ?? []), ...(privateDashboardListQuery?.data?.value ?? [])]); const storeState = reactive({ - favoriteMenuList: computed(() => favoriteStoreGetters.favoriteMenuList), - favoriteWorkspaceMenuList: computed(() => favoriteStoreGetters.workspaceItems), - costDataSource: computed(() => allReferenceStore.getters.costDataSource), isAdminMode: computed(() => appContextStore.getters.isAdminMode), - workspaceList: computed(() => workspaceStoreGetters.workspaceList), currentWorkspaceId: computed(() => workspaceStoreGetters.currentWorkspaceId as string), - projects: computed(() => allReferenceStore.getters.project), - projectGroups: computed(() => allReferenceStore.getters.projectGroup), - cloudServiceTypes: computed(() => allReferenceStore.getters.cloudServiceType), - metrics: computed(() => allReferenceStore.getters.metric), - service: computed(() => allReferenceStore.getters.service), - metricExamples: computed(() => gnbStoreGetters.metricExamples), - costQuerySets: computed(() => gnbStoreGetters.costQuerySets), }); const state = reactive({ active: computed(() => { - const targetList = props.favoriteType === FAVORITE_TYPE.WORKSPACE ? storeState.favoriteWorkspaceMenuList : storeState.favoriteMenuList; + const targetList = props.favoriteType === FAVORITE_TYPE.WORKSPACE ? workspaceFavoriteData.workspaceItems.value : favoriteData.favoriteMenuList.value; const favoriteItem = targetList.findIndex((d) => (d.itemId === props?.itemId && (d.itemType === props.favoriteType))); return favoriteItem > -1; }), }); +const preLoading = computed(() => isLoadingWorkspaceFavoriteData.value || isLoadingConvertedFavoriteData.value); +const triggerLoading = computed(() => isCreatingFavorite.value || isDeletingFavorite.value); + const handleClickFavoriteButton = async (event: MouseEvent) => { if (storeState.isAdminMode) return; event.stopPropagation(); + + if (preLoading.value || triggerLoading.value) return; if (props.readOnly) return; if (state.active) { - await favoriteStore.deleteFavorite({ + await deleteFavorite({ itemType: props.favoriteType, workspaceId: storeState.currentWorkspaceId, itemId: props?.itemId, @@ -121,41 +99,40 @@ const handleClickFavoriteButton = async (event: MouseEvent) => { itemId: props?.itemId, }; const referenceData = convertFavoriteToReferenceData(params); - await favoriteStore.createFavorite(referenceData || params); + await createFavorite(referenceData || params); } emit('click-favorite'); }; const convertFavoriteToReferenceData = (favoriteConfig: FavoriteConfig): ReferenceData|undefined => { const { itemType } = favoriteConfig; if (itemType === FAVORITE_TYPE.DASHBOARD) { - return convertDashboardConfigToReferenceData([favoriteConfig], dashboardList.value)[0]; + return convertedFavoriteData.convertedDashboard.value.find((d) => d.itemId === props?.itemId); } if (itemType === FAVORITE_TYPE.PROJECT) { - return convertProjectConfigToReferenceData([favoriteConfig], storeState.projects)[0]; + return convertedFavoriteData.convertedProject.value.find((d) => d.itemId === props?.itemId); } if (itemType === FAVORITE_TYPE.PROJECT_GROUP) { - return convertProjectGroupConfigToReferenceData([favoriteConfig], storeState.projectGroups)[0]; + return convertedFavoriteData.convertedProjectGroup.value.find((d) => d.itemId === props?.itemId); } if (itemType === FAVORITE_TYPE.CLOUD_SERVICE || itemType === FAVORITE_TYPE.SECURITY) { - return convertCloudServiceConfigToReferenceData([favoriteConfig], storeState.cloudServiceTypes)[0]; + return convertedFavoriteData.convertedCloudServiceType.value.find((d) => d.itemId === props?.itemId); } if (itemType === FAVORITE_TYPE.METRIC) { - return convertMetricConfigToReferenceData([favoriteConfig], storeState.metrics)[0]; + return convertedFavoriteData.convertedMetric.value.find((d) => d.itemId === props?.itemId); } if (itemType === FAVORITE_TYPE.METRIC_EXAMPLE) { - return convertMetricExampleConfigToReferenceData([favoriteConfig], storeState.metricExamples)[0]; + return convertedFavoriteData.convertedMetricExample.value.find((d) => d.itemId === props?.itemId); } if (itemType === FAVORITE_TYPE.COST_ANALYSIS) { - return convertCostAnalysisConfigToReferenceData([favoriteConfig], storeState.costQuerySets, storeState.costDataSource)[0]; + return convertedFavoriteData.convertedCostQuerySet.value.find((d) => d.itemId === props?.itemId); } if (itemType === FAVORITE_TYPE.WORKSPACE) { - return convertWorkspaceConfigToReferenceData([favoriteConfig], storeState.workspaceList)[0]; + return convertedFavoriteData.convertedWorkspace.value.find((d) => d.itemId === props?.itemId); } if (itemType === FAVORITE_TYPE.SERVICE) { - return convertServiceConfigToReferenceData([favoriteConfig], storeState.service)[0]; + return convertedFavoriteData.convertedService.value.find((d) => d.itemId === props?.itemId); } - const allMenuList = getAllMenuList(route, router); - return convertMenuConfigToReferenceData([favoriteConfig], allMenuList)[0]; + return convertedFavoriteData.convertedMenu.value.find((d) => d.itemId === props?.itemId); }; diff --git a/apps/web/src/common/modules/navigations/gnb/GNBToolbox.vue b/apps/web/src/common/modules/navigations/gnb/GNBToolbox.vue index 48e6b0a6ec..61f381ffc3 100644 --- a/apps/web/src/common/modules/navigations/gnb/GNBToolbox.vue +++ b/apps/web/src/common/modules/navigations/gnb/GNBToolbox.vue @@ -25,7 +25,6 @@ import { MENU_ID } from '@/lib/menu/config'; import { useBreadcrumbs } from '@/common/composables/breadcrumbs'; import FavoriteButton from '@/common/modules/favorites/favorite-button/FavoriteButton.vue'; -import { useFavoriteStore } from '@/common/modules/favorites/favorite-button/store/favorite-store'; import { FAVORITE_TYPE } from '@/common/modules/favorites/favorite-button/type'; import type { FavoriteOptions } from '@/common/modules/favorites/favorite-button/type'; import { useGnbStore } from '@/common/modules/navigations/stores/gnb-store'; @@ -38,7 +37,6 @@ const userWorkspaceStore = useUserWorkspaceStore(); const userWorkspaceGetters = userWorkspaceStore.getters; const gnbStore = useGnbStore(); const gnbGetters = gnbStore.getters; -const favoriteStore = useFavoriteStore(); const appContextStore = useAppContextStore(); const appContextGetters = appContextStore.getters; @@ -112,7 +110,6 @@ const handleClickBreadcrumbsDropdownItem = (item: MenuItem) => { watch(() => state.selectedMenuId, async (selectedMenuId) => { await gnbStore.initState(); - await favoriteStore.fetchFavorite(); if (selectedMenuId === MENU_ID.COST_ANALYSIS) { await gnbStore.fetchCostQuerySet(); diff --git a/apps/web/src/common/modules/navigations/top-bar/modules/top-bar-toolset/modules/top-bar-favorite/modules/TopBarFavoriteContextMenu.vue b/apps/web/src/common/modules/navigations/top-bar/modules/top-bar-toolset/modules/top-bar-favorite/modules/TopBarFavoriteContextMenu.vue index 90fd26a1c7..6b7d04cb31 100644 --- a/apps/web/src/common/modules/navigations/top-bar/modules/top-bar-toolset/modules/top-bar-favorite/modules/TopBarFavoriteContextMenu.vue +++ b/apps/web/src/common/modules/navigations/top-bar/modules/top-bar-toolset/modules/top-bar-favorite/modules/TopBarFavoriteContextMenu.vue @@ -3,7 +3,7 @@ import { computed, reactive, } from 'vue'; import type { TranslateResult } from 'vue-i18n'; -import { useRoute, useRouter } from 'vue-router/composables'; +import { useRouter } from 'vue-router/composables'; import { isEmpty } from 'lodash'; @@ -12,7 +12,6 @@ import { } from '@cloudforet/mirinae'; import type { ContextMenuType, MenuItem } from '@cloudforet/mirinae/types/controls/context-menu/type'; -import type { CostQuerySetModel } from '@/api-clients/cost-analysis/cost-query-set/schema/model'; import type { MetricExampleModel } from '@/api-clients/inventory/metric-example/schema/model'; import { i18n } from '@/translations'; @@ -20,30 +19,18 @@ import { useReferenceRouter } from '@/router/composables/use-reference-router'; import { useUserWorkspaceStore } from '@/store/app-context/workspace/user-workspace-store'; import { useAuthorizationStore } from '@/store/authorization/authorization-store'; -import { useAllReferenceStore } from '@/store/reference/all-reference-store'; -import type { CostDataSourceReferenceMap } from '@/store/reference/cost-data-source-reference-store'; -import type { MetricReferenceMap } from '@/store/reference/metric-reference-store'; -import type { ProjectGroupReferenceMap } from '@/store/reference/project-group-reference-store'; -import type { ProjectReferenceMap } from '@/store/reference/project-reference-store'; -import type { ServiceReferenceMap } from '@/store/reference/service-reference-store'; -import type { ReferenceData } from '@/lib/helper/config-data-helper'; import { - convertCostAnalysisConfigToReferenceData, - convertDashboardConfigToReferenceData, - convertMenuConfigToReferenceData, convertMetricConfigToReferenceData, convertMetricExampleConfigToReferenceData, - convertProjectConfigToReferenceData, - convertProjectGroupConfigToReferenceData, convertServiceConfigToReferenceData, getParsedKeysWithManagedCostQueryFavoriteKey, } from '@/lib/helper/config-data-helper'; import type { MenuId, MenuInfo } from '@/lib/menu/config'; import { MENU_ID } from '@/lib/menu/config'; import { MENU_INFO_MAP } from '@/lib/menu/menu-info'; -import { useAllMenuList } from '@/lib/menu/use-all-menu-list'; -import { useGlobalDashboardQuery } from '@/common/composables/global-dashboard/use-global-dashboard-query'; -import { useGrantScopeGuard } from '@/common/composables/grant-scope-guard'; -import { useFavoriteStore } from '@/common/modules/favorites/favorite-button/store/favorite-store'; +import type { ReferenceData } from '@/common/composables/config-data'; +import { useConvertReferencedConfigData } from '@/common/composables/config-data'; +import { useFavoriteDeleteMutation } from '@/common/modules/favorites/core/use-favorite-delete-mutation'; +import { useFavoriteList } from '@/common/modules/favorites/core/use-favorite-list'; import { FAVORITE_TYPE } from '@/common/modules/favorites/favorite-button/type'; import type { FavoriteItem, FavoriteType } from '@/common/modules/favorites/favorite-button/type'; import { useGnbStore } from '@/common/modules/navigations/stores/gnb-store'; @@ -70,39 +57,37 @@ export interface FavoriteMenuItem extends MenuItem { const emit = defineEmits<{(e: 'close'): void; }>(); -const allReferenceStore = useAllReferenceStore(); const userWorkspaceStore = useUserWorkspaceStore(); -const favoriteStore = useFavoriteStore(); -const favoriteGetters = favoriteStore.getters; const gnbStore = useGnbStore(); const gnbStoreGetters = gnbStore.getters; const authorizationStore = useAuthorizationStore(); -const { getAllMenuList } = useAllMenuList(); const { getReferenceLocation } = useReferenceRouter(); -/* Query */ -const { - publicDashboardListQuery, - privateDashboardListQuery, -} = useGlobalDashboardQuery(); + +/* Favorite */ +const { loading: isLoadingFavoriteList, ...favoriteConfigData } = useFavoriteList(); +const { loading: isLoadingConvertedConfigData, ...convertedConfigMap } = useConvertReferencedConfigData({ + allConfigList: favoriteConfigData.favoriteMenuList, + projectConfigList: favoriteConfigData.projectItems, + projectGroupConfigList: favoriteConfigData.projectGroupItems, + metricConfigList: favoriteConfigData.metricItems, + metricExampleConfigList: favoriteConfigData.metricExampleItems, + serviceConfigList: favoriteConfigData.serviceItems, + dashboardConfigList: favoriteConfigData.dashboardItems, + costQuerySetConfigList: favoriteConfigData.costAnalysisItems, + cloudServiceConfigList: favoriteConfigData.cloudServiceTypeItems, +}); +const { mutateAsync: deleteFavorite, isPending: isDeletingFavorite } = useFavoriteDeleteMutation(); const router = useRouter(); -const route = useRoute(); -const dashboardList = computed(() => [...(publicDashboardListQuery?.data?.value ?? []), ...(privateDashboardListQuery?.data?.value ?? [])]); const storeState = reactive({ currentWorkspaceId: computed(() => userWorkspaceStore.getters.currentWorkspaceId), - costDataSource: computed(() => allReferenceStore.getters.costDataSource), - metrics: computed(() => allReferenceStore.getters.metric), metricExamples: computed(() => gnbStoreGetters.metricExamples), - projects: computed(() => allReferenceStore.getters.project), - projectGroups: computed(() => allReferenceStore.getters.projectGroup), - service: computed(() => allReferenceStore.getters.service), - costQuerySets: computed(() => gnbStoreGetters.costQuerySets), pageAccessPermissionList: computed(() => authorizationStore.getters.pageAccessPermissionList), }); const state = reactive({ - loading: true, + loading: computed(() => isLoadingFavoriteList.value || isLoadingConvertedConfigData.value), showAll: false, showAllType: undefined as undefined|FavoriteType, accessProject: computed(() => !isEmpty(authorizationStore.getters.pageAccessPermissionMap[MENU_ID.PROJECT])), @@ -166,6 +151,10 @@ const state = reactive({ items = state.favoriteProjects; label = i18n.t('COMMON.GNB.FAVORITES.ALL_PROJECTS'); } + if (state.showAllType === FAVORITE_TYPE.METRIC) { + items = state.favoriteMetricItems; + label = i18n.t('COMMON.GNB.FAVORITES.ALL_METRIC'); + } if (state.showAllType === FAVORITE_TYPE.COST_ANALYSIS) { items = state.favoriteCostAnalysisItems; label = i18n.t('COMMON.GNB.FAVORITES.ALL_COST_ANALYSIS'); @@ -182,51 +171,37 @@ const state = reactive({ ]; }), // - favoriteMenuItems: computed(() => { - const allMenuList = getAllMenuList(route, router); - return convertMenuConfigToReferenceData( - favoriteGetters.menuItems ?? [], - allMenuList, - ); - }), + favoriteMenuItems: computed(() => convertedConfigMap.convertedMenu.value), favoriteCostAnalysisItems: computed(() => { const isUserAccessible = isUserAccessibleToMenu(MENU_ID.COST_ANALYSIS, storeState.pageAccessPermissionList); - return isUserAccessible - ? convertCostAnalysisConfigToReferenceData( - favoriteGetters.costAnalysisItems ?? [], - storeState.costQuerySets, - storeState.costDataSource, - ) - : []; + if (!isUserAccessible) return []; + return convertedConfigMap.convertedCostQuerySet.value; }), favoriteDashboardItems: computed(() => { const isUserAccessibleToDashboards = isUserAccessibleToMenu(MENU_ID.DASHBOARDS, storeState.pageAccessPermissionList); if (!isUserAccessibleToDashboards) return []; - return convertDashboardConfigToReferenceData( - favoriteGetters.dashboardItems ?? [], - dashboardList.value, - ); + return convertedConfigMap.convertedDashboard.value; }), favoriteMetricItems: computed(() => { const isUserAccessible = isUserAccessibleToMenu(MENU_ID.METRIC_EXPLORER, storeState.pageAccessPermissionList); if (!isUserAccessible) return []; - const favoriteMetricItems = convertMetricConfigToReferenceData(favoriteGetters.metricItems ?? [], storeState.metrics); - const favoriteMetricExampleItems = convertMetricExampleConfigToReferenceData(favoriteGetters.metricExampleItems ?? [], storeState.metricExamples); return [ - ...favoriteMetricItems, - ...favoriteMetricExampleItems, + ...convertedConfigMap.convertedMetric.value, + ...convertedConfigMap.convertedMetricExample.value, ]; }), favoriteProjects: computed(() => { const isUserAccessible = isUserAccessibleToMenu(MENU_ID.PROJECT, storeState.pageAccessPermissionList); if (!isUserAccessible) return []; - const favoriteProjectItems = convertProjectConfigToReferenceData(favoriteGetters.projectItems ?? [], storeState.projects); - const favoriteProjectGroupItems = convertProjectGroupConfigToReferenceData(favoriteGetters.projectGroupItems ?? [], storeState.projectGroups); - return [...favoriteProjectGroupItems, ...favoriteProjectItems]; + return [ + ...convertedConfigMap.convertedProjectGroup.value, + ...convertedConfigMap.convertedProject.value, + ]; }), favoriteServiceItems: computed(() => { const isUserAccessible = isUserAccessibleToMenu(MENU_ID.SERVICE, storeState.pageAccessPermissionList); - return isUserAccessible ? convertServiceConfigToReferenceData(favoriteGetters.serviceItems ?? [], storeState.service) : []; + if (!isUserAccessible) return []; + return convertedConfigMap.convertedService.value; }), }); @@ -309,27 +284,14 @@ const handleSelect = (item: FavoriteMenuItem) => { emit('close'); }; const handleDeleteItem = (item: FavoriteItem) => { - favoriteStore.deleteFavorite({ + if (isDeletingFavorite.value) return; + deleteFavorite({ itemType: item.itemType, workspaceId: storeState.currentWorkspaceId || '', itemId: item.itemId, }); }; -/* Init */ -const init = async () => { - state.loading = true; - await Promise.allSettled([ - favoriteStore.fetchFavorite(), - gnbStore.fetchMetricExample(), - gnbStore.fetchCostQuerySet(), - // HACK: If GNBDashboardMenu is deprecated, you need to add a request to receive a dashboard list here. - ]); - state.loading = false; -}; -const { callApiWithGrantGuard } = useGrantScopeGuard(['WORKSPACE'], init); -callApiWithGrantGuard(); -