Skip to content

Commit 7f1a454

Browse files
committed
Merge branch 'develop' into feature-service-account-budget
Signed-off-by: 이승연 <[email protected]>
2 parents 36ee82f + abd1967 commit 7f1a454

File tree

55 files changed

+200
-149
lines changed

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

55 files changed

+200
-149
lines changed

Diff for: apps/web/package.json

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "web",
3-
"version": "2.0.0-dev349",
3+
"version": "2.0.0-dev350",
44
"private": true,
55
"description": "Cloudforet Console Web Application",
66
"author": "Cloudforet",

Diff for: apps/web/src/api-clients/_common/composables/use-scoped-query.ts

-82
This file was deleted.

Diff for: apps/web/src/common/modules/navigations/top-bar/modules/top-bar-toolset/TopBarToolset.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -38,7 +38,7 @@ const state = reactive({
3838
isDomainAdmin: computed(() => userStore.getters.isDomainAdmin),
3939
isAdminMode: computed(() => appContextStore.getters.isAdminMode),
4040
isGrantLoading: computed(() => appContextStore.getters.globalGrantLoading),
41-
visibleAlertIcon: computed(() => globalConfigStore.state.schema.ALERT_MANAGER.uiAffects?.visibleAlertIcon),
41+
visibleAlertIcon: computed(() => globalConfigStore.state.schema.ALERT_MANAGER?.uiAffects?.visibleAlertIcon),
4242
tooltipTexts: computed<Record<string, string>>(() => ({
4343
adminToggle: (state.isAdminMode ? i18n.t('COMMON.GNB.TOOLTIP.EXIT_ADMIN_MODE') : i18n.t('COMMON.GNB.TOOLTIP.ENABLE_ADMIN_MODE')) as string,
4444
})),

Diff for: apps/web/src/common/modules/navigations/top-bar/modules/top-bar-toolset/modules/top-bar-favorite/TopBarFavorite.vue

-3
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,5 @@
11
<script lang="ts" setup>
22
import { vOnClickOutside } from '@vueuse/components';
3-
import {
4-
defineEmits,
5-
} from 'vue';
63
74
import {
85
PI, PTooltip,

Diff for: apps/web/src/common/modules/widgets/_composables/use-widget-data-table-list-query.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,11 @@ import { computed } from 'vue';
33

44
import type { QueryKey } from '@tanstack/vue-query';
55

6-
import { useScopedQuery } from '@/api-clients/_common/composables/use-scoped-query';
76
import { usePrivateDataTableApi } from '@/api-clients/dashboard/private-data-table/composables/use-private-data-table-api';
87
import { usePublicDataTableApi } from '@/api-clients/dashboard/public-data-table/composables/use-public-data-table-api';
98
import type { DataTableListParameters } from '@/api-clients/dashboard/public-data-table/schema/api-verbs/list';
109
import type { DataTableUpdateParameters } from '@/api-clients/dashboard/public-data-table/schema/api-verbs/update';
10+
import { useScopedQuery } from '@/query/composables/use-scoped-query';
1111
import { useServiceQueryKey } from '@/query/query-key/use-service-query-key';
1212

1313
import type { DataTableModel } from '@/common/modules/widgets/types/widget-data-table-type';

Diff for: apps/web/src/common/modules/widgets/_composables/use-widget-data-table-query.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,10 @@
11
import type { ComputedRef } from 'vue';
22
import { computed } from 'vue';
33

4-
import { useScopedQuery } from '@/api-clients/_common/composables/use-scoped-query';
54
import { usePrivateDataTableApi } from '@/api-clients/dashboard/private-data-table/composables/use-private-data-table-api';
65
import type { DataTableGetParameters } from '@/api-clients/dashboard/private-data-table/schema/api-verbs/get';
76
import { usePublicDataTableApi } from '@/api-clients/dashboard/public-data-table/composables/use-public-data-table-api';
7+
import { useScopedQuery } from '@/query/composables/use-scoped-query';
88
import { useServiceQueryKey } from '@/query/query-key/use-service-query-key';
99

1010

Diff for: apps/web/src/common/modules/widgets/_composables/use-widget-query.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ import { computed } from 'vue';
33

44
import type { QueryKey } from '@tanstack/vue-query';
55

6-
import { useScopedQuery } from '@/api-clients/_common/composables/use-scoped-query';
76
import type { WidgetModel, WidgetUpdateParams } from '@/api-clients/dashboard/_types/widget-type';
87
import { usePrivateWidgetApi } from '@/api-clients/dashboard/private-widget/composables/use-private-widget-api';
98
import type { PrivateWidgetGetParameters } from '@/api-clients/dashboard/private-widget/schema/api-verbs/get';
109
import { usePublicWidgetApi } from '@/api-clients/dashboard/public-widget/composables/use-public-widget-api';
1110
import type { PublicWidgetGetParameters } from '@/api-clients/dashboard/public-widget/schema/api-verbs/get';
11+
import { useScopedQuery } from '@/query/composables/use-scoped-query';
1212
import { useServiceQueryKey } from '@/query/query-key/use-service-query-key';
1313

1414

Diff for: apps/web/src/lib/config/global-config/feature-schema-manager.ts

+2-3
Original file line numberDiff line numberDiff line change
@@ -16,10 +16,10 @@ export class FeatureSchemaManager {
1616
...this.config,
1717
...config,
1818
};
19-
this.createSchema();
19+
this.setFeatureSchema();
2020
}
2121

22-
createSchema() {
22+
setFeatureSchema() {
2323
const globalConfigStore = useGlobalConfigStore();
2424
const schema = {} as FeatureSchemaType;
2525

@@ -32,7 +32,6 @@ export class FeatureSchemaManager {
3232
const currentVersion = this.config[feature]?.VERSION || 'V1';
3333
configurator.initialize(currentVersion);
3434
const menuConfig = configurator.getMenu(this.config);
35-
3635
if (configurator.uiAffect) {
3736
configurator.uiAffect.forEach((uiAffect) => {
3837
const targetFeature = uiAffect.feature;

Diff for: apps/web/src/lib/config/global-config/types/type.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ export interface FeatureConfiguratorType {
3939
getRoutes: () => FeatureRouteConfig;
4040
getMenu: (config?: GlobalServiceConfig) => FeatureMenuConfig;
4141
initialize: (version: FeatureVersion) => void;
42-
uiAffect: FeatureUiAffect[];
42+
uiAffect?: FeatureUiAffect[];
4343
}
4444

4545
interface UiAffectConfig {

Diff for: apps/web/src/lib/site-initializer/index.ts

-2
Original file line numberDiff line numberDiff line change
@@ -27,7 +27,6 @@ import { initDomain } from '@/lib/site-initializer/domain';
2727
import { initDomainSettings } from '@/lib/site-initializer/domain-settings';
2828
import { initEcharts } from '@/lib/site-initializer/echarts';
2929
import { initErrorHandler } from '@/lib/site-initializer/error-handler';
30-
import { initTaskManagementTemplate } from '@/lib/site-initializer/initTaskManagementTemplate';
3130
import { mergeConfig } from '@/lib/site-initializer/merge-config';
3231
import { initModeSetting } from '@/lib/site-initializer/mode-setting';
3332
import { checkSsoAccessToken } from '@/lib/site-initializer/sso';
@@ -106,7 +105,6 @@ const init = async () => {
106105
initRequestIdleCallback();
107106
const results = await Promise.allSettled([
108107
checkSsoAccessToken(),
109-
initTaskManagementTemplate(),
110108
]);
111109
const errors: any[] = [];
112110
results.forEach((result) => {

Diff for: apps/web/src/lib/site-initializer/initTaskManagementTemplate.ts

+1-4
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
import { pinia } from '@/store/pinia';
2-
import { useUserStore } from '@/store/user/user-store';
32

43
import {
54
useTaskManagementTemplateStore,
65
} from '@/services/ops-flow/task-management-templates/stores/use-task-management-template-store';
76

8-
export const initTaskManagementTemplate = async () => {
9-
const userStore = useUserStore(pinia);
10-
if (userStore.state.isSessionExpired) return;
117

8+
export const initTaskManagementTemplate = async () => {
129
const taskManagementTemplateStore = useTaskManagementTemplateStore(pinia);
1310
await Promise.allSettled([
1411
taskManagementTemplateStore.setInitialTemplateId(),

Diff for: apps/web/src/lib/site-initializer/merge-config.ts

+9-2
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
import { SpaceConnector } from '@cloudforet/core-lib/space-connector';
2+
import { APIError } from '@cloudforet/core-lib/space-connector/error';
23

34
import type { PublicConfigGetParameters } from '@/api-clients/config/public-config/schema/api-verbs/get';
45
import { PUBLIC_CONFIG_NAMES } from '@/api-clients/config/public-config/schema/constant';
56
import type { PublicConfigModel } from '@/api-clients/config/public-config/schema/model';
67

78
import type { GlobalServiceConfig } from '@/lib/config/global-config/types/type';
89

10+
import ErrorHandler from '@/common/composables/error/errorHandler';
11+
912
export const mergeConfig = async (config, domainId: string): Promise<GlobalServiceConfig> => {
1013
const baseConfig = config.get('SERVICES') || {};
1114

@@ -16,8 +19,12 @@ export const mergeConfig = async (config, domainId: string): Promise<GlobalServi
1619
domain_id: domainId,
1720
});
1821
overrideConfig = overrideConfigData.SERVICES || {};
19-
} catch {
20-
console.warn('404 Not Found: Data not found. Setting default values.');
22+
} catch (e: any) {
23+
if (e instanceof APIError && e.status === 404) {
24+
console.warn('404 Not Found: Service Setting Data not found. Setting default values.');
25+
} else {
26+
ErrorHandler.handleError(e);
27+
}
2128
}
2229

2330
Object.keys(overrideConfig).forEach((serviceName) => {

Diff for: apps/web/src/query/composables/use-scoped-query.ts

+135
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,135 @@
1+
/**
2+
* useScopedQuery - A custom wrapper around `useQuery` to enforce scope-based API fetching.
3+
*
4+
* ## Why this hook exists?
5+
* This hook was created to integrate **scope-based API access control** with Vue Query.
6+
* It ensures that queries are only executed when the user's granted scope matches the required scope.
7+
* Additionally, it automatically handles loading states and prevents unnecessary queries.
8+
*
9+
* ## Functionality
10+
* - Extends `useQuery` with **grant scope validation**.
11+
* - Runs queries only when **the user's scope is valid** and the app is **ready**.
12+
* - Uses Vue's **reactivity** to dynamically compute the `enabled` state.
13+
* - Supports both **static and reactive `enabled` values**.
14+
*
15+
* ## Parameters:
16+
* - `options`: Standard **Vue Query options** (`UseQueryOptions`).
17+
* - `requiredScopes`: A list of **required grant scopes** to determine if the query should execute.
18+
*
19+
* ## Supported Query Options:
20+
* - `queryKey`, `queryFn`, `select`, `initialData`, `staleTime`, `enabled` (static or reactive)
21+
*
22+
* ## Example:
23+
* const query = useScopedQuery(
24+
* {
25+
* queryKey: ['dashboard', dashboardId],
26+
* queryFn: () => fetchDashboardData(dashboardId),
27+
* enabled: computed(() => isUserAuthorized.value),
28+
* },
29+
* ['DOMAIN', 'WORKSPACE']
30+
* );
31+
*/
32+
33+
import type { MaybeRef } from '@vueuse/core';
34+
import { toValue } from '@vueuse/core';
35+
import { computed, type ComputedRef } from 'vue';
36+
37+
import {
38+
useQuery, type UseQueryOptions, type UseQueryReturnType,
39+
} from '@tanstack/vue-query';
40+
41+
import type { GrantScope } from '@/api-clients/identity/token/schema/type';
42+
import type { QueryKeyArray } from '@/query/query-key/_types/query-key-type';
43+
44+
import { useAppContextStore } from '@/store/app-context/app-context-store';
45+
import { useUserStore } from '@/store/user/user-store';
46+
47+
48+
type ScopedEnabled = MaybeRef<boolean>;
49+
50+
51+
export const useScopedQuery = <TQueryFnData, TError = unknown, TData = TQueryFnData>(
52+
options: UseQueryOptions<TQueryFnData, TError, TData>,
53+
requiredScopes: [GrantScope, ...GrantScope[]],
54+
): UseQueryReturnType<TData, TError> => {
55+
// [Dev Warning] This query is missing `requiredScopes`.
56+
// All scoped queries must explicitly define at least one valid scope for clarity and safety.
57+
if (import.meta.env.DEV && (!requiredScopes || requiredScopes.length === 0)) {
58+
_warnOncePerTick(() => {
59+
console.warn('[useScopedQuery] `requiredScopes` is missing or empty.', {
60+
queryKey: _extractQueryKey((options as any).queryKey),
61+
suggestion: 'Pass at least one valid scope like [\'DOMAIN\'], [\'WORKSPACE\'], etc.',
62+
});
63+
return true;
64+
});
65+
}
66+
67+
const appContextStore = useAppContextStore();
68+
const userStore = useUserStore();
69+
70+
const currentGrantScope = computed<GrantScope | undefined>(
71+
() => userStore.state.currentGrantInfo?.scope,
72+
);
73+
const isAppReady = computed(() => !appContextStore.getters.globalGrantLoading);
74+
75+
const isValidScope = computed(() => currentGrantScope.value !== undefined
76+
&& requiredScopes.includes(currentGrantScope.value));
77+
78+
const rawEnabled = (options as { enabled?: ScopedEnabled }).enabled;
79+
const queryEnabled = computed(() => {
80+
const inheritedEnabled = rawEnabled !== undefined ? toValue(rawEnabled) : true;
81+
return inheritedEnabled && isValidScope.value && isAppReady.value;
82+
});
83+
84+
// [Dev Warning] The current user's scope is not included in the allowed `requiredScopes`.
85+
// This usually indicates a configuration mistake in the query declaration.
86+
if (import.meta.env.DEV) {
87+
const currentScope = currentGrantScope.value;
88+
if (isAppReady.value && currentScope && !requiredScopes.includes(currentScope)) {
89+
_warnOncePerTick(() => {
90+
console.warn('[useScopedQuery] Invalid requiredScopes for current scope:', {
91+
queryKey: _extractQueryKey((options as any).queryKey),
92+
requiredScopes,
93+
currentScope,
94+
});
95+
return true;
96+
});
97+
}
98+
}
99+
100+
return useQuery<TQueryFnData, TError, TData>({
101+
...options,
102+
enabled: queryEnabled,
103+
});
104+
};
105+
106+
const _extractQueryKey = (input: unknown): QueryKeyArray => toValue(input as ComputedRef<QueryKeyArray>);
107+
108+
109+
110+
/* Warning Logger Utilities */
111+
const _warnedKeys = new Set<string>();
112+
const _getCallerKey = (): string => {
113+
try {
114+
const err = new Error();
115+
const stack = err.stack?.split('\n') || [];
116+
117+
const caller = stack.find((line, i) => i > 1
118+
&& (line.includes('.ts') || line.includes('.vue'))
119+
&& !line.includes('use-scoped-query'));
120+
121+
return caller?.trim() ?? 'UNKNOWN_CALLSITE';
122+
} catch {
123+
return 'UNKNOWN_CALLSITE';
124+
}
125+
};
126+
const _warnOncePerTick = (log: () => boolean) => {
127+
const key = _getCallerKey();
128+
if (_warnedKeys.has(key)) return;
129+
const didLog = log();
130+
131+
if (didLog) {
132+
_warnedKeys.add(key);
133+
queueMicrotask(() => _warnedKeys.delete(key));
134+
}
135+
};

Diff for: apps/web/src/query/query-key/_types/query-key-type.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import type { API_DOC } from '@/api-clients/_common/constants/api-doc-constant';
22

33

4-
export type QueryKeyArray = unknown[];
4+
export type QueryKeyArray = readonly unknown[];
55

66
export type QueryScope = 'service' | 'reference';
77

Diff for: apps/web/src/services/alert-manager/v2/components/ServiceDetailTabsSettingsEventRuleCard.vue

+1-1
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ const state = reactive({
9898
result[type].push({
9999
label: i18n.t('ALERT_MANAGER.EVENT_RULE.ASSET_TYPE'),
100100
name: 'asset_types',
101-
value: matchAssetValue.asset_types.map((i) => storeState.cloudServiceType[i].label).join(', '),
101+
value: matchAssetValue.asset_types.map((i) => (storeState.cloudServiceType[i] ? storeState.cloudServiceType[i].label : i)).join(', '),
102102
});
103103
}
104104
if (matchAssetValue.key) {

0 commit comments

Comments
 (0)