Skip to content

feat: add useServiceNavigation #5825

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 1 commit into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
107 changes: 107 additions & 0 deletions apps/web/src/common/composables/service-query/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
import type { Location } from 'vue-router';
import { useRouter } from 'vue-router/composables';

import { useAppContextStore } from '@/store/app-context/app-context-store';
import { useUserWorkspaceStore } from '@/store/app-context/workspace/user-workspace-store';

import type { RouteQueryString } from '@/lib/router-query-string';
import {
primitiveToQueryString, arrayToQueryString, objectToQueryString,
} from '@/lib/router-query-string';

import { serviceNavigationSpecMap } from '@/common/composables/service-query/spec';
import type {
ServiceNavigationSpec, ServiceName, ServiceParamsMap, ServiceQueryMap,
} from '@/common/composables/service-query/type';

export const useServiceNavigation = () => {
const appContextStore = useAppContextStore();
const userWorkspaceStore = useUserWorkspaceStore();
const router = useRouter();

const convertQueryToString = <TQuery extends Record<string, any>>(query: TQuery): Record<string, RouteQueryString> => Object.entries(query).reduce((acc, [key, value]) => {
if (value === undefined) return acc;

if (Array.isArray(value)) {
acc[key] = arrayToQueryString(value);
} else if (typeof value === 'object' && value !== null) {
acc[key] = objectToQueryString(value);
} else if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
acc[key] = primitiveToQueryString(value);
}

return acc;
}, {} as Record<string, RouteQueryString>);

const validateParams = <TParams>(spec: ServiceNavigationSpec<TParams, any>, params: TParams) => {
const requiredParams = spec.params.required as (keyof TParams)[];
const missingParams = requiredParams?.filter((param) => params[param] === undefined);
if (missingParams?.length > 0) {
throw new Error(`Missing required params for ${spec.route.name}: ${missingParams.join(', ')}`);
}
};

const validateQuery = <TQuery>(spec: ServiceNavigationSpec<any, TQuery>, query: TQuery) => {
if (!query) return;

const requiredQueries = spec.query.required as (keyof TQuery)[];
if (requiredQueries) {
const missingQueries = requiredQueries?.filter((q) => query[q] === undefined);
if (missingQueries?.length > 0) {
throw new Error(`Missing required queries for ${spec.route.name}: ${missingQueries.join(', ')}`);
}
}
};

const buildNavigation = <TService extends ServiceName>(
service: TService,
params: ServiceParamsMap[TService],
query: ServiceQueryMap[TService],
): Location => {
const spec = serviceNavigationSpecMap[service];
validateParams(spec, params);
validateQuery<ServiceQueryMap[TService]>(spec, query);

const isAdmin = appContextStore.getters.isAdminMode;
const currentWorkspaceId = userWorkspaceStore.getters.currentWorkspaceId;

const location: Location = {
name: isAdmin ? spec.route.adminName : spec.route.name,
params: {
...(params as unknown as Record<string, string>),
...(isAdmin ? {} : { workspaceId: currentWorkspaceId }),
},
query: convertQueryToString(query),
};

return location;
};

const navigate = <TService extends ServiceName>(
service: TService,
params: ServiceParamsMap[TService],
query: ServiceQueryMap[TService],
options?: { openInNewTab?: boolean },
) => {
const navigation = buildNavigation(service, params, query);
if (options?.openInNewTab) {
window.open(router.resolve(navigation).href, '_blank');
} else {
router.push(navigation).catch(() => {});
}
};

const getLocation = <TService extends ServiceName>(
service: TService,
params: ServiceParamsMap[TService],
query: ServiceQueryMap[TService],
): Location => buildNavigation(service, params, query);

const getHref = <TService extends ServiceName>(
service: TService,
params: ServiceParamsMap[TService],
query: ServiceQueryMap[TService],
): string => router.resolve(buildNavigation(service, params, query)).href;

return { navigate, getLocation, getHref };
};
40 changes: 40 additions & 0 deletions apps/web/src/common/composables/service-query/spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
import type {
CostAnalysisQuery,
ServiceName,
ServiceNavigationSpec,
ServiceParamsMap,
ServiceQueryMap,
} from '@/common/composables/service-query/type';

import { ADMIN_COST_EXPLORER_ROUTE } from '@/services/cost-explorer/routes/admin/route-constant';
import { COST_EXPLORER_ROUTE } from '@/services/cost-explorer/routes/route-constant';


export const serviceNavigationSpecMap: {
[K in ServiceName]: ServiceNavigationSpec<ServiceParamsMap[K], ServiceQueryMap[K]>;
} = {
'cost-analysis': {
route: {
name: COST_EXPLORER_ROUTE.COST_ANALYSIS.QUERY_SET._NAME,
},
params: {
required: ['dataSourceId', 'costQuerySetId'],
optional: ['workspaceId'],
},
query: {
optional: ['granularity', 'group_by', 'period', 'filters'] as (keyof CostAnalysisQuery)[],
},
},
'admin-cost-analysis': {
route: {
name: ADMIN_COST_EXPLORER_ROUTE.COST_ANALYSIS.QUERY_SET._NAME,
},
params: {
required: ['dataSourceId', 'costQuerySetId'],
},
query: {
optional: ['granularity', 'group_by', 'period', 'filters'] as (keyof CostAnalysisQuery)[],
},
},
} as const;

48 changes: 48 additions & 0 deletions apps/web/src/common/composables/service-query/type.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
import type { ConsoleFilter } from '@cloudforet/core-lib/query/type';

import type {
Granularity, GroupBy, Period,
} from '@/services/cost-explorer/types/cost-explorer-query-type';


export type ServiceName = 'cost-analysis' | 'admin-cost-analysis';

/* Cost Analysis */
export interface CostAnalysisParams {
dataSourceId: string;
costQuerySetId: string;
workspaceId?: string;
}
export interface CostAnalysisQuery {
granularity?: Granularity;
group_by?: GroupBy[];
period?: Period;
filters?: ConsoleFilter[];
}

/* Service Navigation Spec */
export interface ServiceNavigationSpec<TParams, TQuery> {
route: {
name: string;
adminName?: string;
};
params: {
required?: (keyof TParams)[];
optional?: (keyof TParams)[];
};
query: {
required?: (keyof TQuery)[];
optional?: (keyof TQuery)[];
};
}

/* Service Params Map */
export type ServiceParamsMap = {
'cost-analysis': CostAnalysisParams;
'admin-cost-analysis': CostAnalysisParams;
};

export type ServiceQueryMap = {
'cost-analysis': CostAnalysisQuery;
'admin-cost-analysis': CostAnalysisQuery;
};
Original file line number Diff line number Diff line change
Expand Up @@ -43,14 +43,14 @@ 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 { useServiceNavigation } from '@/common/composables/service-query';
import { useFavoriteStore } from '@/common/modules/favorites/favorite-button/store/favorite-store';
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';
import TopBarSuggestionList from '@/common/modules/navigations/top-bar/modules/TopBarSuggestionList.vue';

import { ASSET_INVENTORY_ROUTE } from '@/services/asset-inventory/routes/route-constant';
import { COST_EXPLORER_ROUTE } from '@/services/cost-explorer/routes/route-constant';
import { DASHBOARDS_ROUTE } from '@/services/dashboards/routes/route-constant';
import { PROJECT_ROUTE_V2 } from '@/services/project/v2/routes/route-constant';

Expand Down Expand Up @@ -88,6 +88,7 @@ const {

const router = useRouter();
const route = useRoute();
const { navigate } = useServiceNavigation();

const dashboardList = computed(() => [...(publicDashboardListQuery?.data?.value ?? []), ...(privateDashboardListQuery?.data?.value ?? [])]);
const storeState = reactive({
Expand Down Expand Up @@ -298,13 +299,10 @@ const handleSelect = (item: FavoriteMenuItem) => {
} else if (item.itemType === FAVORITE_TYPE.COST_ANALYSIS) {
const dataSourceId = state.favoriteCostAnalysisItems.find((d) => d.name === itemName)?.dataSourceId;
const parsedKeys = getParsedKeysWithManagedCostQueryFavoriteKey(itemName);
router.push({
name: COST_EXPLORER_ROUTE.COST_ANALYSIS.QUERY_SET._NAME,
params: {
dataSourceId,
costQuerySetId: parsedKeys ? parsedKeys[1] : itemName,
},
}).catch(() => {});
navigate('cost-analysis', {
dataSourceId,
costQuerySetId: parsedKeys ? parsedKeys[1] : itemName,
}, {});
}
emit('close');
};
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@ import { showSuccessMessage } from '@/lib/helper/notice-alert-helper';

import ErrorHandler from '@/common/composables/error/errorHandler';
import { usePageEditableStatus } from '@/common/composables/page-editable-status';
import { useServiceNavigation } from '@/common/composables/service-query';
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';
Expand All @@ -31,7 +32,6 @@ import {
DYNAMIC_COST_QUERY_SET_PARAMS,
} from '@/services/cost-explorer/constants/managed-cost-analysis-query-sets';
import { ADMIN_COST_EXPLORER_ROUTE } from '@/services/cost-explorer/routes/admin/route-constant';
import { COST_EXPLORER_ROUTE } from '@/services/cost-explorer/routes/route-constant';
import { useCostAnalysisPageStore } from '@/services/cost-explorer/stores/cost-analysis-page-store';

const CostAnalysisQueryFormModal = () => import('@/services/cost-explorer/components/CostAnalysisQueryFormModal.vue');
Expand All @@ -49,6 +49,7 @@ const appContextStore = useAppContextStore();
const { hasReadWriteAccess } = usePageEditableStatus();

const router = useRouter();
const { navigate } = useServiceNavigation();

const storeState = reactive({
isUnifiedCost: computed(() => costAnalysisPageGetters.isUnifiedCost),
Expand Down Expand Up @@ -97,13 +98,10 @@ const handleDeleteQueryConfirm = async () => {
await SpaceConnector.clientV2.costAnalysis.costQuerySet.delete<CostQuerySetDeleteParameters>({ cost_query_set_id: state.itemIdForDeleteQuery });
await costAnalysisPageStore.listCostQueryList();
showSuccessMessage(i18n.t('BILLING.COST_MANAGEMENT.COST_ANALYSIS.ALT_S_DELETE_QUERY'), '');
await router.push({
name: storeState.isAdminMode ? ADMIN_COST_EXPLORER_ROUTE.COST_ANALYSIS.QUERY_SET._NAME : COST_EXPLORER_ROUTE.COST_ANALYSIS.QUERY_SET._NAME,
params: {
dataSourceId: costAnalysisPageGetters.selectedDataSourceId as string,
costQuerySetId: costAnalysisPageGetters.managedCostQuerySetList[0].cost_query_set_id,
},
}).catch(() => {});
navigate(storeState.isAdminMode ? 'admin-cost-analysis' : 'cost-analysis', {
dataSourceId: costAnalysisPageGetters.selectedDataSourceId as string,
costQuerySetId: costAnalysisPageGetters.managedCostQuerySetList[0].cost_query_set_id,
}, {});
const isFavoriteItem = favoriteGetters.costAnalysisItems.find((item) => item.itemId === state.itemIdForDeleteQuery);
if (isFavoriteItem) {
await favoriteStore.deleteFavorite({
Expand Down
53 changes: 21 additions & 32 deletions apps/web/src/services/cost-explorer/components/CostAnalysisLSB.vue
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<script setup lang="ts">
import { computed, reactive } from 'vue';
import { useRoute, useRouter } from 'vue-router/composables';
import { useRoute } from 'vue-router/composables';

import {
PLazyImg, PSelectDropdown, PI, PToggleButton,
Expand All @@ -18,6 +18,7 @@ import { useUserStore } from '@/store/user/user-store';

import { getCompoundKeyWithManagedCostQuerySetFavoriteKey } from '@/lib/helper/config-data-helper';

import { useServiceNavigation } from '@/common/composables/service-query';
import { useFavoriteStore } from '@/common/modules/favorites/favorite-button/store/favorite-store';
import { FAVORITE_TYPE } from '@/common/modules/favorites/favorite-button/type';
import type { FavoriteConfig } from '@/common/modules/favorites/favorite-button/type';
Expand All @@ -35,8 +36,6 @@ import {
DEFAULT_UNIFIED_COST_CURRENCY, UNIFIED_COST_KEY,
} from '@/services/cost-explorer/constants/cost-explorer-constant';
import { MANAGED_COST_QUERY_SET_ID_LIST } from '@/services/cost-explorer/constants/managed-cost-analysis-query-sets';
import { ADMIN_COST_EXPLORER_ROUTE } from '@/services/cost-explorer/routes/admin/route-constant';
import { COST_EXPLORER_ROUTE } from '@/services/cost-explorer/routes/route-constant';
import { useCostQuerySetStore } from '@/services/cost-explorer/stores/cost-query-set-store';

const DATA_SOURCE_MENU_ID = 'data-source';
Expand All @@ -51,8 +50,8 @@ const favoriteGetters = favoriteStore.getters;
const domainStore = useDomainStore();
const domainGetters = domainStore.getters;

const router = useRouter();
const route = useRoute();
const { getLocation, navigate } = useServiceNavigation();

const appContextStore = useAppContextStore();
const userStore = useUserStore();
Expand All @@ -79,13 +78,10 @@ const state = reactive({
name: 'ic_main-filled',
color: gray[500],
},
to: {
name: storeState.isAdminMode ? ADMIN_COST_EXPLORER_ROUTE.COST_ANALYSIS.QUERY_SET._NAME : COST_EXPLORER_ROUTE.COST_ANALYSIS.QUERY_SET._NAME,
params: {
dataSourceId: storeState.isUnifiedCostOn ? UNIFIED_COST_KEY : (costQuerySetState.selectedDataSourceId ?? ''),
costQuerySetId: d.cost_query_set_id,
},
},
to: getLocation(storeState.isAdminMode ? 'admin-cost-analysis' : 'cost-analysis', {
dataSourceId: storeState.isUnifiedCostOn ? UNIFIED_COST_KEY : (costQuerySetState.selectedDataSourceId ?? ''),
costQuerySetId: d.cost_query_set_id,
}, {}),
favoriteOptions: {
type: FAVORITE_TYPE.COST_ANALYSIS,
id: getCompoundKeyWithManagedCostQuerySetFavoriteKey(d.data_source_id, d.cost_query_set_id),
Expand All @@ -96,13 +92,10 @@ const state = reactive({
type: 'item',
id: d.cost_query_set_id,
label: d.name,
to: {
name: storeState.isAdminMode ? ADMIN_COST_EXPLORER_ROUTE.COST_ANALYSIS.QUERY_SET._NAME : COST_EXPLORER_ROUTE.COST_ANALYSIS.QUERY_SET._NAME,
params: {
dataSourceId: storeState.isUnifiedCostOn ? UNIFIED_COST_KEY : (costQuerySetState.selectedDataSourceId ?? ''),
costQuerySetId: d.cost_query_set_id,
},
},
to: getLocation(storeState.isAdminMode ? 'admin-cost-analysis' : 'cost-analysis', {
dataSourceId: storeState.isUnifiedCostOn ? UNIFIED_COST_KEY : (costQuerySetState.selectedDataSourceId ?? ''),
costQuerySetId: d.cost_query_set_id,
}, {}),
favoriteOptions: {
type: FAVORITE_TYPE.COST_ANALYSIS,
},
Expand Down Expand Up @@ -160,24 +153,20 @@ const filterStarredItems = (menuItems: LSBItem[] = []): LSBItem[] => menuItems.f
const handleSelectDataSource = (selected: string) => {
if (!selected) return;
costQuerySetStore.setSelectedDataSourceId(selected);
router.push({
name: storeState.isAdminMode ? ADMIN_COST_EXPLORER_ROUTE.COST_ANALYSIS.QUERY_SET._NAME : COST_EXPLORER_ROUTE.COST_ANALYSIS.QUERY_SET._NAME,
params: {
dataSourceId: selected,
costQuerySetId: costQuerySetGetters.managedCostQuerySets[0].cost_query_set_id,
},
}).catch(() => {});

navigate(storeState.isAdminMode ? 'admin-cost-analysis' : 'cost-analysis', {
dataSourceId: selected,
costQuerySetId: costQuerySetGetters.managedCostQuerySets[0].cost_query_set_id,
}, {});
};

const handleSelectUnifiedCostToggle = (value: boolean) => {
costQuerySetStore.setUnifiedCostOn(value);
router.push({
name: storeState.isAdminMode ? ADMIN_COST_EXPLORER_ROUTE.COST_ANALYSIS.QUERY_SET._NAME : COST_EXPLORER_ROUTE.COST_ANALYSIS.QUERY_SET._NAME,
params: {
dataSourceId: value ? UNIFIED_COST_KEY : (costQuerySetState.selectedDataSourceId ?? UNIFIED_COST_KEY),
costQuerySetId: costQuerySetGetters.managedCostQuerySets[0].cost_query_set_id,
},
}).catch(() => {});

navigate(storeState.isAdminMode ? 'admin-cost-analysis' : 'cost-analysis', {
dataSourceId: value ? UNIFIED_COST_KEY : (costQuerySetState.selectedDataSourceId ?? UNIFIED_COST_KEY),
costQuerySetId: costQuerySetGetters.managedCostQuerySets[0].cost_query_set_id,
}, {});
};

(() => {
Expand Down
Loading
Loading