Skip to content
Merged
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
4 changes: 4 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -23,3 +23,7 @@ dev/*.env
user.json
.vercel
.env*.local

# Claude
.claude/*
CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,8 +75,10 @@ const handleClickSaveButton = async () => {
hours: serviceAccountPageFormState.scheduleHours,
},
sync_options: {
skip_project_group: serviceAccountPageFormState.skipProjectGroup,
project_group_mapping_type: serviceAccountPageFormState.projectGroupMappingType,
single_workspace_id: serviceAccountPageFormState.selectedSingleWorkspace ?? undefined,
custom_depth: serviceAccountPageFormState.customDepth ?? 1,

Copilot AI Nov 11, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The custom_depth field is sent with a default value of 1 when it's null or undefined (?? 1). However, this may not be appropriate for all workspace mapping types. When workspace_mapping_type is not CUSTOM_DEPTH_GROUPS, sending a custom_depth value could be misleading or incorrect.

Consider only including custom_depth in the payload when the workspace mapping type is CUSTOM_DEPTH_GROUPS, or document why a default of 1 is always appropriate.

Suggested change
custom_depth: serviceAccountPageFormState.customDepth ?? 1,
...(serviceAccountPageFormState.workspaceMappingType === 'CUSTOM_DEPTH_GROUPS'
? { custom_depth: serviceAccountPageFormState.customDepth ?? 1 }
: {}),

Copilot uses AI. Check for mistakes.
workspace_mapping_type: serviceAccountPageFormState.workspaceMappingType,
},
plugin_options: serviceAccountPageFormState.additionalOptions,
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,12 @@ const state = reactive({
additionalOptions: {},
isScheduleHoursValid: computed(() => ((state.isAutoSyncEnabled) ? !!serviceAccountPageFormState.scheduleHours.length : true)),
isAdditionalOptionsValid: false,
isMappingMethodValid: false,
isAllValid: computed(() => {
if (!state.isAutoSyncEnabled) return true;
return state.isScheduleHoursValid && (serviceAccountPageStore.getters.autoSyncAdditionalOptionsSchema ? state.isAdditionalOptionsValid : true);
return state.isScheduleHoursValid
&& (serviceAccountPageStore.getters.autoSyncAdditionalOptionsSchema ? state.isAdditionalOptionsValid : true)
&& state.isMappingMethodValid;
}),
});

Expand Down Expand Up @@ -76,11 +79,18 @@ const handleAdditionalOptionsValidate = (isValid:boolean) => {
state.isAdditionalOptionsValid = state.isAutoSyncEnabled ? isValid : true;
};

const handleMappingMethodValidate = (isValid:boolean) => {
state.isMappingMethodValid = state.isAutoSyncEnabled ? isValid : true;
};

const handleChangeToggle = (e:boolean) => {
serviceAccountPageStore.$patch((_state) => {
_state.formState.isAutoSyncEnabled = e;
});
// Auto Sync가 비활성화되면 mapping method validation을 즉시 true로 설정
Comment thread
seungyeoneeee marked this conversation as resolved.
if (!e) {
state.isMappingMethodValid = true;
}
};

watch(() => state.additionalOptions, (additionalOptions) => {
Expand All @@ -91,7 +101,7 @@ watch(() => state.isAllValid, (isAllValid) => {
serviceAccountPageStore.$patch((_state) => {
_state.formState.isAutoSyncFormValid = isAllValid;
});
});
}, { immediate: true });
</script>

<template>
Expand All @@ -108,7 +118,9 @@ watch(() => state.isAllValid, (isAllValid) => {
</p>
</div>
<div v-if="state.isAutoSyncEnabled">
<service-account-auto-sync-mapping-method mode="UPDATE" />
<service-account-auto-sync-mapping-method mode="UPDATE"
@update:is-valid="handleMappingMethodValidate"
/>
<div v-if="serviceAccountPageStore.getters.autoSyncAdditionalOptionsSchema">
<p-field-title label="Additional Options"
size="lg"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,13 +34,23 @@ const props = withDefaults(defineProps<{mode:'UPDATE'|'READ'}>(), {
mode: 'UPDATE',
});

const emit = defineEmits<{(event: 'update:is-valid', value: boolean): void;
}>();

const serviceAccountPageStore = useServiceAccountPageStore();
const serviceAccountPageState = serviceAccountPageStore.state;
const appContextStore = useAppContextStore();
const userWorkspaceStore = useUserWorkspaceStore();

const state = reactive({
selectedWorkspace: computed(() => serviceAccountPageStore.formState.selectedSingleWorkspace ?? ''),
selectedWorkspace: computed({
get: () => serviceAccountPageStore.formState.selectedSingleWorkspace ?? '',
set: (value: string) => {
serviceAccountPageStore.$patch((_state) => {
_state.formState.selectedSingleWorkspace = value;
});
},
}),
organizationTerms: computed<{ name: string; group: string }>(() => CSP_ORGANIZATION_TERMS[serviceAccountPageState.selectedProvider] ?? {}),
workspaceMapping: WORKSPACE_MAPPING_TYPE.ALL_GROUPS_SINGLE_WORKSPACE as WorkspaceMappingType,
projectGroupMapping: PROJECT_GROUP_MAPPING_TYPE.SKIP as ProjectGroupMappingType,
Expand Down Expand Up @@ -93,21 +103,36 @@ const state = reactive({
selectedWorkspaceMappingOptionLabel: computed<string>(() => {
const option = WORKSPACE_MAPPING_OPTIONS.find((opt) => opt.value === state.workspaceMapping);
if (!option) return '';
return option.target ? `${option.name} ${option.target}` : option.name;
return option.target ? `${option.name} ${option.target}` : option.name;
}),
selectedProjectGroupMappingOptionLabel: computed<string>(() => {
const option = PROJECT_GROUP_MAPPING_OPTIONS.find((opt) => opt.value === state.projectGroupMapping);
if (!option) return '';
return option.target ? `${option.name} ${option.target}` : option.name;
return option.target ? `${option.name} ${option.target}` : option.name;
}),
customDepthMaxDepth: computed<number>(() => CUSTOM_DEPTH_MAX_DEPTH[serviceAccountPageState.selectedProvider]),
customDepthInvalid: computed<boolean>(() => state.customDepth !== null && (state.customDepth < 1 || state.customDepth > state.customDepthMaxDepth)),
// Validation states
isWorkspaceMappingValid: computed<boolean>(() => {
if (state.workspaceMapping === WORKSPACE_MAPPING_TYPE.ALL_GROUPS_SINGLE_WORKSPACE) {
return !!state.selectedWorkspace;
}
return true;
}),
isProjectGroupMappingValid: computed<boolean>(() => {
if (state.workspaceMapping === WORKSPACE_MAPPING_TYPE.CUSTOM_DEPTH_GROUPS) {
return !state.customDepthInvalid && state.customDepth !== null;
}
return true;
}),
isMappingMethodValid: computed<boolean>(() => {
if (!state.isDomainForm) return true;
return state.isWorkspaceMappingValid && state.isProjectGroupMappingValid;
}),
});

const handleUpdateWorkspace = (workspaceId:string) => {
serviceAccountPageStore.$patch((_state) => {
_state.formState.selectedSingleWorkspace = workspaceId;
});
state.selectedWorkspace = workspaceId;
};

watch(() => state.formData, (formData) => {
Expand All @@ -116,14 +141,40 @@ watch(() => state.formData, (formData) => {
});
});

// Initialize from store formState (for CREATE mode) or originServiceAccountItem (for UPDATE mode)
watch(() => serviceAccountPageState.originServiceAccountItem, (item) => {
if (item) {
state.workspaceMapping = item.sync_options?.workspace_mapping_type ?? WORKSPACE_MAPPING_TYPE.ALL_GROUPS_SINGLE_WORKSPACE;
state.projectGroupMapping = item.sync_options?.project_group_mapping_type ?? PROJECT_GROUP_MAPPING_TYPE.NESTED_SUB_GROUPS;
state.customDepth = item.sync_options?.custom_depth ?? 1;
state.customDepth = item.sync_options?.custom_depth ?? null;
}
}, { immediate: true });

// Sync workspaceMapping from formState on mount (for cases where store is already populated)
watch(() => serviceAccountPageStore.formState.workspaceMappingType, (newType) => {
if (newType && newType !== state.workspaceMapping) {
state.workspaceMapping = newType;
}
}, { immediate: true });

// Sync projectGroupMapping from formState on mount
watch(() => serviceAccountPageStore.formState.projectGroupMappingType, (newType) => {
if (newType && newType !== state.projectGroupMapping) {
state.projectGroupMapping = newType;
}
}, { immediate: true });

// Sync customDepth from formState on mount
watch(() => serviceAccountPageStore.formState.customDepth, (newDepth) => {
if (newDepth !== undefined && newDepth !== state.customDepth) {
state.customDepth = newDepth;
}
}, { immediate: true });

Comment thread
seungyeoneeee marked this conversation as resolved.
// Emit validation state whenever any validation-related state changes
watch(() => state.isMappingMethodValid, (isValid) => {
emit('update:is-valid', isValid);
}, { immediate: true });
</script>

<template>
Expand Down Expand Up @@ -205,19 +256,23 @@ watch(() => serviceAccountPageState.originServiceAccountItem, (item) => {
</div>
<div v-else>
<div class="flex gap-1 flex-wrap items-center">
<span>{{ state.selectedWorkspaceMappingOptionLabel }} ➔</span>
<div v-if="state.selectedWorkspace"
class="flex gap-1"
>
<!-- All Groups → workspace name -->
<template v-if="state.workspaceMapping === WORKSPACE_MAPPING_TYPE.ALL_GROUPS_SINGLE_WORKSPACE && state.selectedWorkspace">
<span>All Groups ➔</span>
<workspace-logo-icon :text="state.selectedWorkspaceItem?.name || ''"
:theme="state.selectedWorkspaceItem?.tags?.theme"
size="xs"
/>
<span class="workspace-name">{{ state.selectedWorkspaceItem?.name }}</span>
</div>
<div v-else>
Multiple workspaces
</div>
</template>
<!-- Custom Depth Groups → Multiple workspaces -->
<template v-else-if="state.workspaceMapping === WORKSPACE_MAPPING_TYPE.CUSTOM_DEPTH_GROUPS && state.customDepth">
<span>Groups of {{ state.customDepth }} depth ➔ Multiple workspaces</span>
</template>
<!-- Other cases: use label as is -->
<template v-else>
<span>{{ state.selectedWorkspaceMappingOptionLabel }}</span>
</template>
</div>
</div>
</div>
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ import type { ListResponse } from '@/schema/_common/api-verbs/list';
import type { ServiceAccountListParameters } from '@/schema/identity/service-account/api-verbs/list';
import { ACCOUNT_TYPE } from '@/schema/identity/service-account/constant';
import type { ServiceAccountModel } from '@/schema/identity/service-account/model';
import type { TrustedAccountListParameters } from '@/schema/identity/trusted-account/api-verbs/list';
import type { TrustedAccountModel } from '@/schema/identity/trusted-account/model';
import { i18n } from '@/translations';

import type { Tag } from '@/common/components/forms/tags-input-group/type';
import TagsInput from '@/common/components/inputs/TagsInput.vue';
import ErrorHandler from '@/common/composables/error/errorHandler';
import { useFormValidator } from '@/common/composables/form-validator';

import ServiceAccountProjectForm from '@/services/asset-inventory/components/ServiceAccountProjectForm.vue';
Expand Down Expand Up @@ -48,7 +50,7 @@ const {
serviceAccountName: (val: string) => {
if (val?.length < 2) {
return i18n.t('IDENTITY.SERVICE_ACCOUNT.ADD.NAME_INVALID');
} if (state.serviceAccountNames.includes(val)) {
} if (state.allAccountNames.includes(val)) {
if (state.originForm?.accountName === val) return true;
return i18n.t('IDENTITY.SERVICE_ACCOUNT.ADD.NAME_DUPLICATED');
}
Expand All @@ -68,6 +70,9 @@ const state = reactive({
}),
})),
serviceAccountNames: [] as string[],
trustedAccountNames: [] as string[],
allAccountNames: computed<string[]>(() => [...state.serviceAccountNames, ...state.trustedAccountNames]),
isLoadingNames: false,
customSchemaForm: {},
isCustomSchemaFormValid: undefined,
tags: {},
Expand All @@ -80,10 +85,25 @@ const state = reactive({
projectForm: state.projectForm,
tags: state.tags,
})),
isAllValid: computed(() => ((invalidState.serviceAccountName === false)
&& (state.isTrustedAccount ? true : state.isProjectFormValid)
&& state.isTagsValid
&& (isEmpty(props.schema) ? true : state.isCustomSchemaFormValid))),
isAllValid: computed(() => {
// Name validation: must be valid (false means valid in invalidState)
const isNameValid = invalidState.serviceAccountName === false;

// Account name must have a value (not empty)
const hasName = !!serviceAccountName.value;

// Project form validation (only for non-trusted accounts)
const isProjectValid = state.isTrustedAccount ? true : state.isProjectFormValid;

// Tags validation
const isTagsValidValue = state.isTagsValid;

// Custom schema validation (account_id etc.)
// When schema exists, validation must be true (not undefined or false)
const isSchemaValid = isEmpty(props.schema) ? true : state.isCustomSchemaFormValid === true;

return isNameValid && hasName && isProjectValid && isTagsValidValue && isSchemaValid;
}),
isChanged: false,
});

Expand All @@ -99,12 +119,43 @@ const initFormData = (originForm: Partial<BaseInformationForm>) => {

/* Api */
const listServiceAccounts = async () => {
const { results } = await SpaceConnector.clientV2.identity.serviceAccount.list<ServiceAccountListParameters, ListResponse<ServiceAccountModel>>({
query: {
only: ['name'],
},
});
state.serviceAccountNames = (results ?? []).map((v) => v.name);
try {
const { results } = await SpaceConnector.clientV2.identity.serviceAccount.list<ServiceAccountListParameters, ListResponse<ServiceAccountModel>>({
query: {
only: ['name'],
},
});
state.serviceAccountNames = (results ?? []).map((v) => v.name);
} catch (e) {
ErrorHandler.handleError(e);
state.serviceAccountNames = [];
}
};

const listTrustedAccounts = async () => {
try {
const { results } = await SpaceConnector.clientV2.identity.trustedAccount.list<TrustedAccountListParameters, ListResponse<TrustedAccountModel>>({
query: {
only: ['name'],
},
});
state.trustedAccountNames = (results ?? []).map((v) => v.name);
} catch (e) {
ErrorHandler.handleError(e);
state.trustedAccountNames = [];
}
};

const fetchAllAccountNames = async () => {
state.isLoadingNames = true;
try {
await Promise.all([
listServiceAccounts(),
listTrustedAccounts(),
]);
} finally {
state.isLoadingNames = false;
}
};

/* Event */
Expand All @@ -131,21 +182,23 @@ const handleChangeProjectForm = (projectForm) => {

/* Init */
(async () => {
await listServiceAccounts();
await fetchAllAccountNames();
})();

/* Watcher */
watch([() => state.isAllValid, () => state.isChanged], ([isAllValid, isChanged]) => {
if (props.mode === 'UPDATE' && !isChanged) {
// UPDATE 모드: validation + changed 모두 체크
Comment thread
seungyeoneeee marked this conversation as resolved.
if (props.mode === 'UPDATE') {
serviceAccountPageStore.$patch((_state) => {
_state.formState.isBaseInformationFormValid = false;
_state.formState.isBaseInformationFormValid = isAllValid && isChanged;
});
return;
}
// CREATE 모드: validation만 체크
Comment thread
seungyeoneeee marked this conversation as resolved.
serviceAccountPageStore.$patch((_state) => {
_state.formState.isBaseInformationFormValid = isAllValid;
});
});
}, { immediate: true });
watch(() => state.formData, (formData) => {
serviceAccountPageStore.$patch((_state) => {
_state.formState.baseInformation = formData;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -235,7 +235,7 @@ watch(() => formState.isAllValid, (isAllValid) => {
serviceAccountPageStore.$patch((_state) => {
_state.formState.isCredentialFormValid = isAllValid;
});
});
}, { immediate: true });
const schemaApiQueryHelper = new ApiQueryHelper();
const getSecretSchema = async (isTrustingSchema:boolean) => {
const trustedAccountRelatedSchemas = isTrustingSchema ? serviceAccountSchemaStore.getters.trustingSecretSchemaList : (storeState.trustedAccountSchema?.related_schemas ?? []);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,11 @@ const state = reactive({
pageLimit: PAGE_SIZE,
});

// Watch for prop changes to sync internal state
watch(() => props.selected, (newSelected) => {
state.selected = newSelected;
});

const fetchWorkspace = async (searchText?:string) => {
try {
state.loading = true;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -133,8 +133,10 @@ const createAccount = async (): Promise<string|undefined> => {
hours: serviceAccountPageFormState.scheduleHours,
},
sync_options: {
skip_project_group: serviceAccountPageFormState.skipProjectGroup,
project_group_mapping_type: serviceAccountPageFormState.projectGroupMappingType,
single_workspace_id: serviceAccountPageFormState.selectedSingleWorkspace ?? undefined,
custom_depth: serviceAccountPageFormState.customDepth ?? 1,

Copilot AI Nov 11, 2025

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The custom_depth field is sent with a default value of 1 when it's null or undefined (?? 1). However, this may not be appropriate for all workspace mapping types. When workspace_mapping_type is not CUSTOM_DEPTH_GROUPS, sending a custom_depth value could be misleading or incorrect.

Consider only including custom_depth in the payload when the workspace mapping type is CUSTOM_DEPTH_GROUPS, or document why a default of 1 is always appropriate.

Suggested change
custom_depth: serviceAccountPageFormState.customDepth ?? 1,
...(serviceAccountPageFormState.workspaceMappingType === 'CUSTOM_DEPTH_GROUPS'
? { custom_depth: serviceAccountPageFormState.customDepth ?? 1 }
: {}),

Copilot uses AI. Check for mistakes.
workspace_mapping_type: serviceAccountPageFormState.workspaceMappingType,
},
plugin_options: serviceAccountPageFormState.additionalOptions,
});
Expand Down
Loading
Loading