diff --git a/apps/web/src/api-clients/identity/trusted-account/schema/api-verbs/create.ts b/apps/web/src/api-clients/identity/trusted-account/schema/api-verbs/create.ts index bedf702ac3..f36dfb2066 100644 --- a/apps/web/src/api-clients/identity/trusted-account/schema/api-verbs/create.ts +++ b/apps/web/src/api-clients/identity/trusted-account/schema/api-verbs/create.ts @@ -1,5 +1,6 @@ import type { Tags } from '@/api-clients/_common/schema/model'; import type { ResourceGroupType } from '@/api-clients/_common/schema/type'; +import type { AzureManagementGroupMappingType } from '@/api-clients/identity/trusted-account/schema/type'; export interface TrustedAccountCreateParameters { name: string; @@ -14,6 +15,7 @@ export interface TrustedAccountCreateParameters { sync_options?: { skip_project_group: boolean; single_workspace_id: string; + azure_management_group_mapping_type?: AzureManagementGroupMappingType; // only for Azure }; plugin_options?: Record; tags?: Tags; diff --git a/apps/web/src/api-clients/identity/trusted-account/schema/api-verbs/update.ts b/apps/web/src/api-clients/identity/trusted-account/schema/api-verbs/update.ts index 3f6e252d51..42c2972d74 100644 --- a/apps/web/src/api-clients/identity/trusted-account/schema/api-verbs/update.ts +++ b/apps/web/src/api-clients/identity/trusted-account/schema/api-verbs/update.ts @@ -1,4 +1,5 @@ import type { Tags } from '@/api-clients/_common/schema/model'; +import type { AzureManagementGroupMappingType } from '@/api-clients/identity/trusted-account/schema/type'; export interface TrustedAccountUpdateParameters { trusted_account_id: string; @@ -11,6 +12,7 @@ export interface TrustedAccountUpdateParameters { sync_options?: { skip_project_group: boolean; single_workspace_id: string; + azure_management_group_mapping_type?: AzureManagementGroupMappingType; // only for Azure }; plugin_options?: Record; tags?: Tags; diff --git a/apps/web/src/api-clients/identity/trusted-account/schema/constant.ts b/apps/web/src/api-clients/identity/trusted-account/schema/constant.ts new file mode 100644 index 0000000000..f4871efe58 --- /dev/null +++ b/apps/web/src/api-clients/identity/trusted-account/schema/constant.ts @@ -0,0 +1,4 @@ +export const AZURE_MANAGEMENT_GROUP_MAPPING_TYPE = { + TOP_MANAGEMENT_GROUP: 'Top Management Group', + LEAF_MANAGEMENT_GROUP: 'Leaf Management Group', +} as const; diff --git a/apps/web/src/api-clients/identity/trusted-account/schema/model.ts b/apps/web/src/api-clients/identity/trusted-account/schema/model.ts index e01e0d73d5..fb312466eb 100644 --- a/apps/web/src/api-clients/identity/trusted-account/schema/model.ts +++ b/apps/web/src/api-clients/identity/trusted-account/schema/model.ts @@ -1,5 +1,6 @@ import type { Tags } from '@/api-clients/_common/schema/model'; import type { ResourceGroupType } from '@/api-clients/_common/schema/type'; +import type { AzureManagementGroupMappingType } from '@/api-clients/identity/trusted-account/schema/type'; export interface TrustedAccountModel { trusted_account_id: string; @@ -13,6 +14,8 @@ export interface TrustedAccountModel { sync_options?: { skip_project_group: boolean; single_workspace_id: string; + use_management_group_as_workspace?: boolean; // only for Azure + azure_management_group_mapping_type?: AzureManagementGroupMappingType; // only for Azure }; plugin_options?: Record; tags: Tags; diff --git a/apps/web/src/api-clients/identity/trusted-account/schema/type.ts b/apps/web/src/api-clients/identity/trusted-account/schema/type.ts new file mode 100644 index 0000000000..979cf9cf3b --- /dev/null +++ b/apps/web/src/api-clients/identity/trusted-account/schema/type.ts @@ -0,0 +1,3 @@ +import type { AZURE_MANAGEMENT_GROUP_MAPPING_TYPE } from '@/api-clients/identity/trusted-account/schema/constant'; + +export type AzureManagementGroupMappingType = (typeof AZURE_MANAGEMENT_GROUP_MAPPING_TYPE)[keyof typeof AZURE_MANAGEMENT_GROUP_MAPPING_TYPE]; diff --git a/apps/web/src/common/components/info-tooltip/InfoTooltip.vue b/apps/web/src/common/components/info-tooltip/InfoTooltip.vue new file mode 100644 index 0000000000..5274de29c2 --- /dev/null +++ b/apps/web/src/common/components/info-tooltip/InfoTooltip.vue @@ -0,0 +1,35 @@ + + + diff --git a/apps/web/src/services/service-account/components/ServiceAccountAutoSync.vue b/apps/web/src/services/service-account/components/ServiceAccountAutoSync.vue index f0bfb2dfcf..b8def26780 100644 --- a/apps/web/src/services/service-account/components/ServiceAccountAutoSync.vue +++ b/apps/web/src/services/service-account/components/ServiceAccountAutoSync.vue @@ -77,6 +77,7 @@ const handleClickSaveButton = async () => { sync_options: { skip_project_group: serviceAccountPageFormState.skipProjectGroup, single_workspace_id: serviceAccountPageFormState.selectedSingleWorkspace ?? undefined, + azure_management_group_mapping_type: serviceAccountPageFormState.azureManagementGroupMappingType ?? undefined, }, plugin_options: serviceAccountPageFormState.additionalOptions, }); diff --git a/apps/web/src/services/service-account/components/ServiceAccountAutoSyncMappingMethod.vue b/apps/web/src/services/service-account/components/ServiceAccountAutoSyncMappingMethod.vue index 7586074292..6a417a3f04 100644 --- a/apps/web/src/services/service-account/components/ServiceAccountAutoSyncMappingMethod.vue +++ b/apps/web/src/services/service-account/components/ServiceAccountAutoSyncMappingMethod.vue @@ -4,75 +4,32 @@ import { computed, reactive, watch } from 'vue'; import { PFieldTitle, PRadio } from '@cloudforet/mirinae'; + +import type { TrustedAccountModel } from '@/api-clients/identity/trusted-account/schema/model'; +import { i18n } from '@/translations'; + import { useAppContextStore } from '@/store/app-context/app-context-store'; import { useUserWorkspaceStore } from '@/store/app-context/workspace/user-workspace-store'; +import InfoTooltip from '@/common/components/info-tooltip/InfoTooltip.vue'; import MappingMethod from '@/common/components/mapping-method/MappingMethod.vue'; +import type { MappingItem } from '@/common/components/mapping-method/type'; import WorkspaceLogoIcon from '@/common/modules/navigations/top-bar/modules/top-bar-header/WorkspaceLogoIcon.vue'; import WorkspaceDropdown from '@/services/service-account/components/WorkspaceDropdown.vue'; +import { CSP_AUTO_SYNC_OPTIONS_MAP, WORKSPACE_MAPPING_OPTIONS_MAP } from '@/services/service-account/constants/auto-sync-options-contant'; +import type { ServiceAccountStoreFormState } from '@/services/service-account/stores/service-account-page-store'; import { useServiceAccountPageStore } from '@/services/service-account/stores/service-account-page-store'; -const cspAdditionalOptionMap = { - aws: { - name: 'AWS Organization', - workspaceMappingOptions: [ - { - label: 'Top-level Organization Units', - value: 'multipleWorkspaces', - }, - { - label: 'AWS Organization', - value: 'singleWorkspace', - }, - ], - projectGroupMappingOptions: [ - { - label: 'Nested Organization Units', - value: 'projectGroups', - }, - ], - }, - azure: { - name: 'Azure Tenant', - workspaceMappingOptions: [ - { - label: 'Multitenant Organization', - value: 'multipleWorkspaces', - }, - { - label: 'Azure Tenant', - value: 'singleWorkspace', - }, - ], - projectGroupMappingOptions: [ - { - label: 'Nested Management Groups', - value: 'projectGroups', - }, - ], - }, - google_cloud: { - name: 'Google Cloud Organization', - workspaceMappingOptions: [ - { - label: 'Top-level Folders in Google Cloud Organization', - value: 'multipleWorkspaces', - }, - { - label: 'Google Cloud Organization', - value: 'singleWorkspace', - }, - ], - projectGroupMappingOptions: [ - { - label: 'Folders in Google Cloud Organization', - value: 'projectGroups', - }, - ], - }, +type FormData = Partial>; +type MappingMethodOptionType = { + label: string; + value: any; + info?: string; }; +type WorkspaceMapping = (typeof WORKSPACE_MAPPING_OPTIONS_MAP)[keyof typeof WORKSPACE_MAPPING_OPTIONS_MAP]; + const props = withDefaults(defineProps<{mode:'UPDATE'|'READ'}>(), { mode: 'UPDATE', }); @@ -83,52 +40,156 @@ const serviceAccountPageFormState = serviceAccountPageStore.formState; const appContextStore = useAppContextStore(); const userWorkspaceStore = useUserWorkspaceStore(); + const state = reactive({ - selectedWorkspace: computed(() => serviceAccountPageStore.formState.selectedSingleWorkspace ?? ''), - additionalOptionUiByProvider: computed(() => cspAdditionalOptionMap[serviceAccountPageState.selectedProvider] ?? {}), - workspaceMapping: 'multipleWorkspaces', - projectGroupMapping: 'projectGroups', + selectedWorkspace: computed(() => serviceAccountPageStore.formState.selectedSingleWorkspace ?? undefined), + mappingMethodProviderLabel: computed(() => CSP_AUTO_SYNC_OPTIONS_MAP[serviceAccountPageState.selectedProvider]?.name ?? ''), + workspaceMappingOptions: computed(() => CSP_AUTO_SYNC_OPTIONS_MAP[serviceAccountPageState.selectedProvider].workspaceMappingOptions), + projectGroupMappingOptions: computed(() => [ + ...CSP_AUTO_SYNC_OPTIONS_MAP[serviceAccountPageState.selectedProvider].projectGroupMappingOptions, + { + label: i18n.t('IDENTITY.SERVICE_ACCOUNT.AUTO_SYNC.SKIP_PROJECT_GROUP_MAPPING'), + value: false, + }, + ]), + workspaceMapping: 'multi' as WorkspaceMapping, + projectGroupMappingDisabled: computed(() => serviceAccountPageState.selectedProvider === 'azure' + && state.workspaceMapping === WORKSPACE_MAPPING_OPTIONS_MAP.LEAF_AZURE_MANAGEMENT_GROUP_MAPPING), + projectGroupMapping: true as boolean, + + formData: computed(() => convertToMappingMethodDTO(state.isDomainForm, state.workspaceMapping, state.projectGroupMapping, state.selectedWorkspace)), selectedWorkspaceItem: computed(() => userWorkspaceStore.getters.workspaceMap[state.selectedWorkspace] ?? {}), isAdminMode: computed(() => appContextStore.getters.isAdminMode), isResourceGroupDomain: computed(() => serviceAccountPageState.originServiceAccountItem.resource_group === 'DOMAIN'), isCreatePage: computed(() => serviceAccountPageState.originServiceAccountItem?.resource_group === undefined), isDomainForm: computed(() => (state.isCreatePage ? state.isAdminMode : state.isResourceGroupDomain)), - mappingItems: computed(() => (state.isDomainForm ? [ - { - imageUrl: serviceAccountPageStore.getters.selectedProviderItem?.icon, - name: 'provider', - }, - { - icon: 'ic_workspaces', - name: 'workspace', - }, - { - icon: 'ic_document-filled', - name: 'project_group', - }, - ] : [ - { - icon: 'ic_document-filled', - name: 'project_group', - }, - ])), - formData: computed(() => (state.isDomainForm ? { - selectedSingleWorkspace: state.workspaceMapping === 'singleWorkspace' ? state.selectedWorkspace : '', - skipProjectGroup: state.projectGroupMapping === 'skip', - } : { - skipProjectGroup: state.projectGroupMapping === 'skip', - })), - selectedWorkspaceMappingOptionLabel: computed(() => cspAdditionalOptionMap[serviceAccountPageState.selectedProvider].workspaceMappingOptions - .find((option) => (option.value === (state.selectedWorkspace ? 'singleWorkspace' : 'multipleWorkspaces')))?.label), - selectedProjectGroupMappingOptionLabel: computed(() => cspAdditionalOptionMap[serviceAccountPageState.selectedProvider].projectGroupMappingOptions[0].label), + mappingItems: computed(() => { + if (state.isDomainForm) { + return [ + { + imageUrl: serviceAccountPageStore.getters.selectedProviderItem?.icon, + name: 'provider', + }, + { + icon: 'ic_workspaces', + name: 'workspace', + }, + { + icon: 'ic_document-filled', + name: 'project_group', + }, + ].filter((item) => (state.projectGroupMappingDisabled ? item.name !== 'project_group' : true)); + } + return [ + { + icon: 'ic_document-filled', + name: 'project_group', + }, + ]; + }), + selectedWorkspaceMappingOptionLabel: computed(() => CSP_AUTO_SYNC_OPTIONS_MAP[serviceAccountPageState.selectedProvider].workspaceMappingOptions + .find((option) => (option.value === state.workspaceMapping))?.label), + selectedProjectGroupMappingOptionLabel: computed(() => CSP_AUTO_SYNC_OPTIONS_MAP[serviceAccountPageState.selectedProvider].projectGroupMappingOptions[0].label), }); + +/* Utils */ +/* +* This functions are used to convert the mapping method form data to the service account form data. +* And also, convert the service account form data to the mapping method form data. +* +* 1. convertToMappingMethodDTO +* 2. convertToMappingMethodClientEntity +* +* Azure Account has additional mapping method options (Top Node Management Group, Leaf Node Management Group). +*/ +const convertToMappingMethodDTO = (isDomainForm: boolean, workspaceMapping: WorkspaceMapping, projectGroupMapping: boolean, selectedWorkspace: string) => { + // In Workspace Tenant + if (!isDomainForm) { + return { + skipProjectGroup: !projectGroupMapping, + }; + } + // In Admin Mode : Multi Workspace + if (workspaceMapping === WORKSPACE_MAPPING_OPTIONS_MAP.MULTI) { + return { + azureManagementGroupMappingType: undefined, + skipProjectGroup: !projectGroupMapping, + selectedSingleWorkspace: undefined, + }; + } + // In Admin Mode : Single Workspace + if (workspaceMapping === WORKSPACE_MAPPING_OPTIONS_MAP.SINGLE) { + return { + skipProjectGroup: !projectGroupMapping, + azureManagementGroupMappingType: undefined, + selectedSingleWorkspace: selectedWorkspace, + }; + } + // In Admin Mode : (Only Azure) Top Node Management Group + if (workspaceMapping === WORKSPACE_MAPPING_OPTIONS_MAP.TOP_AZURE_MANAGEMENT_GROUP_MAPPING) { + return { + skipProjectGroup: !projectGroupMapping, + azureManagementGroupMappingType: workspaceMapping, + selectedSingleWorkspace: undefined, + }; + } + // In Admin Mode : (Only Azure) Leaf Node Management Group + if (workspaceMapping === WORKSPACE_MAPPING_OPTIONS_MAP.LEAF_AZURE_MANAGEMENT_GROUP_MAPPING) { + return { + skipProjectGroup: !projectGroupMapping, + azureManagementGroupMappingType: workspaceMapping, + selectedSingleWorkspace: undefined, + }; + } + // default + return { + skipProjectGroup: !projectGroupMapping, + selectedSingleWorkspace: undefined, + azureManagementGroupMappingType: undefined, + }; +}; +const convertToMappingMethodClientEntity = (originServiceAccountItem: TrustedAccountModel) => { + const skipProjectGroup = originServiceAccountItem.sync_options?.skip_project_group; + const singleWorkspaceId = originServiceAccountItem.sync_options?.single_workspace_id; + const azureManagementGroupMappingType = originServiceAccountItem.sync_options?.azure_management_group_mapping_type; + + // Multi Workspace + if (!singleWorkspaceId && !azureManagementGroupMappingType) { + state.workspaceMapping = WORKSPACE_MAPPING_OPTIONS_MAP.MULTI; + state.projectGroupMapping = !skipProjectGroup; + // Azure Multi Management Group Workspace + } else if (azureManagementGroupMappingType) { + state.workspaceMapping = azureManagementGroupMappingType; + state.projectGroupMapping = azureManagementGroupMappingType === WORKSPACE_MAPPING_OPTIONS_MAP.LEAF_AZURE_MANAGEMENT_GROUP_MAPPING ? true : !skipProjectGroup; + // Single Workspace + } else if (singleWorkspaceId) { + state.workspaceMapping = WORKSPACE_MAPPING_OPTIONS_MAP.SINGLE; + state.projectGroupMapping = !skipProjectGroup; + // Default (Workspace Tenant) + } else { + state.projectGroupMapping = !skipProjectGroup; + } +}; + + + +/* Event */ const handleUpdateWorkspace = (workspaceId:string) => { serviceAccountPageStore.$patch((_state) => { _state.formState.selectedSingleWorkspace = workspaceId; }); }; +const handleWorkspaceMappingChange = (value: WorkspaceMapping) => { + state.workspaceMapping = value; + if (value === WORKSPACE_MAPPING_OPTIONS_MAP.LEAF_AZURE_MANAGEMENT_GROUP_MAPPING) state.projectGroupMapping = false; +}; + +const handleProjectGroupMappingChange = (value: boolean) => { + state.projectGroupMapping = value; +}; + watch(() => state.formData, (formData) => { Object.entries(formData).forEach(([key, value]) => { serviceAccountPageStore.setFormState(key, value); @@ -137,8 +198,8 @@ watch(() => state.formData, (formData) => { watch(() => serviceAccountPageState.originServiceAccountItem, (item) => { if (item) { - state.workspaceMapping = item.sync_options?.single_workspace_id ? 'singleWorkspace' : 'multipleWorkspaces'; - state.projectGroupMapping = item.sync_options?.skip_project_group ? 'skip' : 'projectGroups'; + const _item = item as TrustedAccountModel; + convertToMappingMethodClientEntity(_item); } }, { immediate: true }); @@ -154,7 +215,7 @@ watch(() => serviceAccountPageState.originServiceAccountItem, (item) => { class="mb-6" >