| 1 |
<template> |
| 2 |
<div class="space-y-6"> |
| 3 |
<div v-if="fetching" class="fct-card fct-card-border overflow-hidden mb-0"> |
| 4 |
<div class="fct-card-body"> |
| 5 |
<el-skeleton animated :rows="6" /> |
| 6 |
</div> |
| 7 |
</div> |
| 8 |
|
| 9 |
<template v-else> |
| 10 |
<div v-if="showDashboard" class="space-y-6"> |
| 11 |
<div class="fct-storage-activated-card"> |
| 12 |
<div class="activated-card-header"> |
| 13 |
<div class="activated-card-content"> |
| 14 |
<img |
| 15 |
v-if="payload.logo" |
| 16 |
:class="{ 'dark:hidden': payload.dark_logo }" |
| 17 |
:src="payload.logo" |
| 18 |
:alt="payload.driver_label" |
| 19 |
/> |
| 20 |
<img |
| 21 |
v-if="payload.dark_logo" |
| 22 |
:src="payload.dark_logo" |
| 23 |
:alt="payload.driver_label" |
| 24 |
class="hidden dark:block" |
| 25 |
/> |
| 26 |
|
| 27 |
<div |
| 28 |
v-if="!payload.logo && !payload.dark_logo" |
| 29 |
class="bucket-name-logo" |
| 30 |
> |
| 31 |
S3 |
| 32 |
</div> |
| 33 |
|
| 34 |
<div class="activated-card-content-inner"> |
| 35 |
<div class="activated-card-bucket-name"> |
| 36 |
<h2 class="title"> |
| 37 |
{{ payload.driver_label }} |
| 38 |
</h2> |
| 39 |
|
| 40 |
<span :class="['badge small', form.is_active === 'yes' ? 'success' : 'info']"> |
| 41 |
{{ form.is_active === 'yes' ? translate('Active') : translate('Inactive') }} |
| 42 |
</span> |
| 43 |
|
| 44 |
<span class="badge small info"> |
| 45 |
{{ connectionMethodLabel }} |
| 46 |
</span> |
| 47 |
</div> |
| 48 |
|
| 49 |
<p class="activated-card-bucket-desc"> |
| 50 |
{{ payload.description }} |
| 51 |
</p> |
| 52 |
|
| 53 |
<!-- <div class="mt-2 flex flex-wrap items-center gap-2 text-sm text-system-mid dark:text-gray-300 hidden"> |
| 54 |
<a |
| 55 |
v-if="bucketConsoleUrl" |
| 56 |
:href="bucketConsoleUrl" |
| 57 |
target="_blank" |
| 58 |
rel="noopener noreferrer" |
| 59 |
class="font-medium text-primary-500 no-underline hover:underline dark:text-gray-50" |
| 60 |
> |
| 61 |
{{ form.bucket }} |
| 62 |
</a> |
| 63 |
<span v-else class="font-medium text-system-light dark:text-gray-400"> |
| 64 |
{{ translate('No bucket selected') }} |
| 65 |
</span> |
| 66 |
<span v-if="form.region" class="text-system-light dark:text-gray-400">/</span> |
| 67 |
<span v-if="form.region">{{ formattedRegion }}</span> |
| 68 |
</div> --> |
| 69 |
|
| 70 |
<!-- btn group --> |
| 71 |
<div class="fct-btn-group mt-3"> |
| 72 |
<LoadingButton size="small" :loading="checking_connection" @click="checkConnection"> |
| 73 |
{{ translate('Check again') }} |
| 74 |
</LoadingButton> |
| 75 |
|
| 76 |
<el-button size="small" @click="enableEditMode"> |
| 77 |
{{ translate('Edit Config') }} |
| 78 |
</el-button> |
| 79 |
|
| 80 |
<el-popconfirm |
| 81 |
:title="translate('Are you sure you want to reset all settings? This cannot be undone.')" |
| 82 |
:confirm-button-text="translate('Yes, Reset')" |
| 83 |
:cancel-button-text="translate('No')" |
| 84 |
confirm-button-type="danger" |
| 85 |
@confirm="resetSettings" |
| 86 |
> |
| 87 |
<template #reference> |
| 88 |
<el-button size="small" plain type="danger"> |
| 89 |
{{ translate('Reset') }} |
| 90 |
</el-button> |
| 91 |
</template> |
| 92 |
</el-popconfirm> |
| 93 |
</div> |
| 94 |
</div> |
| 95 |
</div> |
| 96 |
|
| 97 |
<div class="shrink-0"> |
| 98 |
<el-switch |
| 99 |
v-model="form.is_active" |
| 100 |
active-value="yes" |
| 101 |
inactive-value="no" |
| 102 |
:loading="saving" |
| 103 |
@change="handleSwitchChange" |
| 104 |
/> |
| 105 |
</div> |
| 106 |
</div> |
| 107 |
|
| 108 |
<div class="activated-card-body"> |
| 109 |
<ul class="fct-activated-bucket-list-info"> |
| 110 |
<li> |
| 111 |
<div class="info-title"> |
| 112 |
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">> |
| 113 |
<path d="M4.66659 4.66667H11.1666C12.571 4.66667 13.2733 4.66667 13.7777 5.00373C13.9961 5.14964 14.1836 5.33715 14.3295 5.55553C14.6666 6.05997 14.6666 6.7622 14.6666 8.16667C14.6666 10.5074 14.6666 11.6778 14.1048 12.5186C13.8616 12.8825 13.5491 13.195 13.1852 13.4382C12.3444 14 11.174 14 8.83325 14H7.99992C4.85722 14 3.28587 14 2.30956 13.0237C1.33325 12.0474 1.33325 10.476 1.33325 7.33333V5.29618C1.33325 4.0852 1.33325 3.47971 1.5868 3.02538C1.76753 2.70151 2.03476 2.43428 2.35863 2.25354C2.81296 2 3.41845 2 4.62943 2C5.40527 2 5.79318 2 6.13276 2.12734C6.90807 2.41808 7.22778 3.12238 7.57763 3.82208L7.99992 4.66667" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/> |
| 114 |
</svg> |
| 115 |
{{ translate('Bucket') }} |
| 116 |
</div> |
| 117 |
<div class="info-value"> |
| 118 |
<a |
| 119 |
v-if="bucketConsoleUrl" |
| 120 |
:href="bucketConsoleUrl" |
| 121 |
target="_blank" |
| 122 |
rel="noopener noreferrer" |
| 123 |
> |
| 124 |
{{ form.bucket }} |
| 125 |
</a> |
| 126 |
<span v-else> |
| 127 |
{{ translate('No bucket selected') }} |
| 128 |
</span> |
| 129 |
</div> |
| 130 |
</li> |
| 131 |
|
| 132 |
<li> |
| 133 |
<div class="info-title"> |
| 134 |
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">> |
| 135 |
<g clip-path="url(#clip0_22443_64899)"> |
| 136 |
<circle cx="7.99992" cy="7.9987" r="6.66667" stroke="currentColor" stroke-width="1.5"/> |
| 137 |
<ellipse cx="7.99992" cy="7.9987" rx="2.66667" ry="6.66667" stroke="currentColor" stroke-width="1.5"/> |
| 138 |
<path d="M1.33325 8H14.6666" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> |
| 139 |
</g> |
| 140 |
<defs> |
| 141 |
<clipPath id="clip0_22443_64899"> |
| 142 |
<rect width="16" height="16" fill="white"/> |
| 143 |
</clipPath> |
| 144 |
</defs> |
| 145 |
</svg> |
| 146 |
{{ translate('Region') }} |
| 147 |
</div> |
| 148 |
<div class="info-value"> |
| 149 |
{{ form.region ? formattedRegion : translate('Not selected') }} |
| 150 |
</div> |
| 151 |
</li> |
| 152 |
|
| 153 |
<li> |
| 154 |
<div class="info-title"> |
| 155 |
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">> |
| 156 |
<g clip-path="url(#clip0_22443_64906)"> |
| 157 |
<path d="M4.66659 1.33203C2.82564 1.33203 1.33325 2.82442 1.33325 4.66536C1.33325 5.89917 2.00358 6.97641 2.99992 7.55276V11.8941C2.99992 12.4391 2.99992 12.7116 3.10141 12.9567C3.20291 13.2017 3.39559 13.3944 3.78097 13.7797L4.66659 14.6654L6.07205 13.2599C6.13687 13.1951 6.1693 13.1627 6.19618 13.1275C6.26679 13.0351 6.31203 12.9259 6.3274 12.8107C6.33325 12.7668 6.33325 12.721 6.33325 12.6293C6.33325 12.5551 6.33325 12.518 6.32932 12.482C6.31901 12.3874 6.28861 12.2962 6.24015 12.2144C6.22167 12.1833 6.19941 12.1536 6.15489 12.0942L5.33325 10.9987L5.79992 10.3765C6.06424 10.024 6.19641 9.84782 6.26483 9.64256C6.33325 9.43729 6.33325 9.21702 6.33325 8.77648V7.55276C7.32959 6.97641 7.99992 5.89917 7.99992 4.66536C7.99992 2.82442 6.50753 1.33203 4.66659 1.33203Z" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/> |
| 158 |
<path d="M4.66675 4.66797H4.67274" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/> |
| 159 |
<path d="M8.66675 9.33203H12.6667C13.288 9.33203 13.5986 9.33203 13.8437 9.43353C14.1704 9.56885 14.4299 9.82842 14.5653 10.1551C14.6667 10.4001 14.6667 10.7108 14.6667 11.332C14.6667 11.9533 14.6667 12.2639 14.5653 12.5089C14.4299 12.8356 14.1704 13.0952 13.8437 13.2305C13.5986 13.332 13.288 13.332 12.6667 13.332H8.66675" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/> |
| 160 |
<path d="M10 3.33203H12.6667C13.2879 3.33203 13.5985 3.33203 13.8436 3.43353C14.1703 3.56885 14.4298 3.82842 14.5652 4.15512C14.6667 4.40015 14.6667 4.71078 14.6667 5.33203C14.6667 5.95329 14.6667 6.26391 14.5652 6.50894C14.4298 6.83565 14.1703 7.09521 13.8436 7.23054C13.5985 7.33203 13.2879 7.33203 12.6667 7.33203H10" stroke="currentColor" stroke-width="1.5" stroke-linecap="round"/> |
| 161 |
</g> |
| 162 |
<defs> |
| 163 |
<clipPath id="clip0_22443_64906"> |
| 164 |
<rect width="16" height="16" fill="white"/> |
| 165 |
</clipPath> |
| 166 |
</defs></svg> |
| 167 |
{{ translate('Authentication') }} |
| 168 |
</div> |
| 169 |
<div class="info-value"> |
| 170 |
{{ connectionMethodLabel }} |
| 171 |
</div> |
| 172 |
</li> |
| 173 |
|
| 174 |
<li> |
| 175 |
<div class="info-title"> |
| 176 |
<svg width="16" height="16" viewBox="0 0 16 16" fill="none" xmlns="http://www.w3.org/2000/svg">> |
| 177 |
<g clip-path="url(#clip0_22443_64941)"> |
| 178 |
<path d="M7.99888 1.33203C5.9937 1.33203 4.69354 2.67803 3.15589 3.16863C2.53067 3.36812 2.21806 3.46786 2.09155 3.60846C1.96504 3.74907 1.92799 3.95453 1.8539 4.36545C1.06104 8.76269 2.79401 12.828 6.92694 14.4103C7.371 14.5804 7.59303 14.6654 8.00107 14.6654C8.40911 14.6654 8.63112 14.5804 9.07515 14.4103C13.2078 12.828 14.9392 8.76268 14.1461 4.36545C14.0719 3.95446 14.0349 3.74896 13.9083 3.60836C13.7818 3.46775 13.4692 3.36806 12.844 3.16869C11.3058 2.67813 10.0041 1.33203 7.99888 1.33203Z" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> |
| 179 |
<path d="M6 8.66667C6 8.66667 6.66667 8.66667 7.33333 10C7.33333 10 9.45098 6.66667 11.3333 6" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/> |
| 180 |
</g> |
| 181 |
<defs> |
| 182 |
<clipPath id="clip0_22443_64941"> |
| 183 |
<rect width="16" height="16" fill="white"/> |
| 184 |
</clipPath> |
| 185 |
</defs></svg> |
| 186 |
{{ translate('Security posture') }} |
| 187 |
</div> |
| 188 |
<div class="info-value"> |
| 189 |
{{ form.block_public_access === 'yes' ? translate('Public access blocked') : translate('Public access open') }}, |
| 190 |
{{ form.object_ownership === 'yes' ? translate('ownership enforced') : translate('ownership not enforced') }} |
| 191 |
|
| 192 |
<span :class="['badge small', securityRecommendationLabel === translate('Recommended') ? 'success' : 'warning']"> |
| 193 |
{{ securityRecommendationLabel }} |
| 194 |
</span> |
| 195 |
</div> |
| 196 |
</li> |
| 197 |
</ul> |
| 198 |
</div> |
| 199 |
|
| 200 |
|
| 201 |
</div> |
| 202 |
</div> |
| 203 |
|
| 204 |
<div v-else class="space-y-6"> |
| 205 |
<el-alert |
| 206 |
v-if="showMissingDefineCredentialsWarning" |
| 207 |
:title="missingDefineCredentialsMessage" |
| 208 |
type="warning" |
| 209 |
show-icon |
| 210 |
:closable="false" |
| 211 |
/> |
| 212 |
|
| 213 |
<div class="fct-card"> |
| 214 |
<div class="fct-card-header border-bottom"> |
| 215 |
<StepIndicator |
| 216 |
:steps="visibleSteps" |
| 217 |
:current-step="stepperActive + 1" |
| 218 |
@step-click="handleStepClick" |
| 219 |
/> |
| 220 |
</div> |
| 221 |
|
| 222 |
<div class="fct-card-body"> |
| 223 |
<div v-show="currentStep === 1" class="fct-step-content-box space-y-4"> |
| 224 |
<h4 class="fct-step-content-box-heading"> |
| 225 |
{{ translate('Verify Access Keys') }} |
| 226 |
</h4> |
| 227 |
<ul class="fct-coverage-selector"> |
| 228 |
<li |
| 229 |
:class="{ active: form.auth_method === 'define' }" |
| 230 |
@click="!form.defined_in_wp_config && (form.auth_method = 'define')" |
| 231 |
> |
| 232 |
<div class="w-full"> |
| 233 |
<div class="fct-coverage-selector-content-wrap"> |
| 234 |
<div class="fct-coverage-selector-content"> |
| 235 |
<span class="fct-coverage-selector-title"> |
| 236 |
{{ translate('Define access keys in wp-config.php') }} |
| 237 |
<span class="opacity-50">{{ translate('(Recommended)') }}</span> |
| 238 |
</span> |
| 239 |
<span class="fct-coverage-selector-desc"> |
| 240 |
{{ form.defined_in_wp_config |
| 241 |
? translate('You have already defined your credentials in wp-config.php.') |
| 242 |
: translate('Recommended. Keeps your credentials secure by defining them in your configuration file.') }} |
| 243 |
</span> |
| 244 |
</div> |
| 245 |
<div class="fct-coverage-selector-dot-wrap shrink-0"> |
| 246 |
<span class="fct-coverage-selector-dot"></span> |
| 247 |
</div> |
| 248 |
</div> |
| 249 |
|
| 250 |
<Animation :visible="form.auth_method === 'define'" accordion :duration="220"> |
| 251 |
<div class="mt-4"> |
| 252 |
<div class="space-y-4"> |
| 253 |
<div class="config-snippet"> |
| 254 |
<pre class="m-0 overflow-x-auto text-xs text-system-dark dark:text-gray-50">{{ wpConfigSnippet }}</pre> |
| 255 |
</div> |
| 256 |
|
| 257 |
<div class="flex justify-start"> |
| 258 |
<el-button @click.stop="copyCode">{{ translate('Copy Snippet') }}</el-button> |
| 259 |
</div> |
| 260 |
</div> |
| 261 |
</div> |
| 262 |
</Animation> |
| 263 |
</div> |
| 264 |
</li> |
| 265 |
|
| 266 |
<li |
| 267 |
:class="{ active: form.auth_method === 'db', 'opacity-60 cursor-not-allowed': form.defined_in_wp_config }" |
| 268 |
@click="!form.defined_in_wp_config && (form.auth_method = 'db')" |
| 269 |
> |
| 270 |
<div class="w-full"> |
| 271 |
<div class="fct-coverage-selector-content-wrap"> |
| 272 |
<div class="fct-coverage-selector-content"> |
| 273 |
<span class="fct-coverage-selector-title">{{ translate('Store credentials in the database') }}</span> |
| 274 |
<span class="fct-coverage-selector-desc"> |
| 275 |
{{ translate('Store credentials in the database. Less secure than other methods.') }} |
| 276 |
</span> |
| 277 |
<span v-if="form.defined_in_wp_config" class="mt-2 block text-sm text-system-mid dark:text-gray-300"> |
| 278 |
{{ translate('The database credential option is unavailable while your access keys are defined in wp-config.php.') }} |
| 279 |
</span> |
| 280 |
</div> |
| 281 |
<div class="fct-coverage-selector-dot-wrap shrink-0"> |
| 282 |
<span class="fct-coverage-selector-dot"></span> |
| 283 |
</div> |
| 284 |
</div> |
| 285 |
|
| 286 |
<Animation :visible="form.auth_method === 'db'" accordion :duration="220"> |
| 287 |
<div class="mt-4"> |
| 288 |
<div class="grid grid-cols-1 gap-4 lg:grid-cols-2"> |
| 289 |
<div> |
| 290 |
<label class="mb-2 block text-sm font-medium text-system-dark dark:text-gray-50">{{ translate('Access Key') }}</label> |
| 291 |
<el-input v-model="form.access_key" :placeholder="translate('Enter access key')" @click.stop /> |
| 292 |
</div> |
| 293 |
|
| 294 |
<div> |
| 295 |
<label class="mb-2 block text-sm font-medium text-system-dark dark:text-gray-50">{{ translate('Secret Key') }}</label> |
| 296 |
<el-input v-model="form.secret_key" show-password :placeholder="translate('Enter secret key')" @click.stop /> |
| 297 |
</div> |
| 298 |
</div> |
| 299 |
</div> |
| 300 |
</Animation> |
| 301 |
</div> |
| 302 |
</li> |
| 303 |
</ul> |
| 304 |
</div> |
| 305 |
|
| 306 |
<div v-show="currentStep === 2" class="fct-step-content-box space-y-4"> |
| 307 |
<h4 class="fct-step-content-box-heading"> |
| 308 |
{{ translate('Choose where files live') }} |
| 309 |
</h4> |
| 310 |
|
| 311 |
<ul class="fct-coverage-selector"> |
| 312 |
<li :class="{ active: bucketAction === 'existing' }" @click="bucketAction = 'existing'"> |
| 313 |
<div class="w-full"> |
| 314 |
<div class="fct-coverage-selector-content-wrap"> |
| 315 |
<div class="fct-coverage-selector-content"> |
| 316 |
<span class="fct-coverage-selector-title">{{ translate('Connect an existing bucket') }}</span> |
| 317 |
<span class="fct-coverage-selector-desc">{{ translate('Choose a bucket you already manage in this AWS account.') }}</span> |
| 318 |
</div> |
| 319 |
<div class="fct-coverage-selector-dot-wrap shrink-0"> |
| 320 |
<span class="fct-coverage-selector-dot"></span> |
| 321 |
</div> |
| 322 |
</div> |
| 323 |
|
| 324 |
<Animation :visible="bucketAction === 'existing'" accordion :duration="220"> |
| 325 |
<div class="mt-4"> |
| 326 |
<div class="space-y-4"> |
| 327 |
<div class="space-y-2"> |
| 328 |
<span class="block text-sm font-medium text-system-dark dark:text-gray-50">{{ translate('Bucket name') }}</span> |
| 329 |
<div class="fct-coverage-bucket-name-selectors"> |
| 330 |
<label @click.stop> |
| 331 |
<el-radio v-model="bucketInputMode" value="enter" @click.stop /> |
| 332 |
<span>{{ translate('Enter bucket name') }}</span> |
| 333 |
</label> |
| 334 |
|
| 335 |
<label @click.stop> |
| 336 |
<el-radio v-model="bucketInputMode" value="select" @click.stop /> |
| 337 |
<span>{{ translate('Select from existing') }}</span> |
| 338 |
</label> |
| 339 |
|
| 340 |
</div> |
| 341 |
</div> |
| 342 |
|
| 343 |
<div v-if="bucketInputMode === 'enter'"> |
| 344 |
<el-input v-model="form.bucket" :placeholder="translate('Enter bucket name')" @click.stop /> |
| 345 |
</div> |
| 346 |
|
| 347 |
<div v-else class="fct-bucket-list-wrap"> |
| 348 |
<div class="fct-bucket-list-search"> |
| 349 |
<el-input |
| 350 |
v-model="bucketSearchQuery" |
| 351 |
clearable |
| 352 |
:placeholder="translate('Search buckets')" |
| 353 |
@input="handleBucketSearch" |
| 354 |
@clear="handleBucketSearch" |
| 355 |
@click.stop |
| 356 |
/> |
| 357 |
</div> |
| 358 |
|
| 359 |
<div class="fct-bucket-list-header"> |
| 360 |
<span class="selected-bucket-count"> |
| 361 |
{{ bucketOptions.length }} {{ translate(bucketOptions.length === 1 ? 'bucket' : 'buckets') }} |
| 362 |
</span> |
| 363 |
<el-button text class="px-0" :loading="bucketLoading" @click.stop="refreshBucketList"> |
| 364 |
{{ translate('Reload bucket list') }} |
| 365 |
</el-button> |
| 366 |
</div> |
| 367 |
|
| 368 |
<div v-if="bucketLoading" class="px-4 py-5 text-sm text-system-mid dark:text-gray-300"> |
| 369 |
{{ translate('Loading buckets...') }} |
| 370 |
</div> |
| 371 |
|
| 372 |
<div v-else-if="!bucketOptions.length" class="px-4 py-5 text-sm text-system-mid dark:text-gray-300"> |
| 373 |
{{ translate('No buckets found or credentials are invalid.') }} |
| 374 |
</div> |
| 375 |
|
| 376 |
<div v-else-if="!filteredBucketOptions.length" class="px-4 py-5 text-sm text-system-mid dark:text-gray-300"> |
| 377 |
{{ translate('No buckets match your search.') }} |
| 378 |
</div> |
| 379 |
|
| 380 |
<div v-else :class="bucketListClasses" class="fct-bucket-list"> |
| 381 |
<button |
| 382 |
v-for="item in filteredBucketOptions" |
| 383 |
:key="item.value" |
| 384 |
type="button" |
| 385 |
:class="form.bucket === item.value ? 'is-bucket-selected' : ''" |
| 386 |
@click.stop="selectBucket(item)" |
| 387 |
> |
| 388 |
<span class="min-w-0 flex-1"> |
| 389 |
<span class="bucket-name"> |
| 390 |
<svg width="20" height="20" viewBox="0 0 20 20" fill="none" xmlns="http://www.w3.org/2000/svg">> |
| 391 |
<path d="M4.75 10.375C4.75 10.6097 5.09575 11.0185 5.8975 11.4197C6.9355 11.9387 8.40775 12.25 10 12.25C11.5923 12.25 13.0645 11.9387 14.1025 11.4197C14.9043 11.0185 15.25 10.6097 15.25 10.375V8.74675C14.0125 9.51175 12.1203 10 10 10C7.87975 10 5.9875 9.511 4.75 8.74675V10.375ZM15.25 12.4968C14.0125 13.2618 12.1203 13.75 10 13.75C7.87975 13.75 5.9875 13.261 4.75 12.4968V14.125C4.75 14.3597 5.09575 14.7685 5.8975 15.1697C6.9355 15.6887 8.40775 16 10 16C11.5923 16 13.0645 15.6887 14.1025 15.1697C14.9043 14.7685 15.25 14.3597 15.25 14.125V12.4968ZM3.25 14.125V6.625C3.25 4.76125 6.2725 3.25 10 3.25C13.7275 3.25 16.75 4.76125 16.75 6.625V14.125C16.75 15.9887 13.7275 17.5 10 17.5C6.2725 17.5 3.25 15.9887 3.25 14.125ZM10 8.5C11.5923 8.5 13.0645 8.18875 14.1025 7.66975C14.9043 7.2685 15.25 6.85975 15.25 6.625C15.25 6.39025 14.9043 5.9815 14.1025 5.58025C13.0645 5.06125 11.5923 4.75 10 4.75C8.40775 4.75 6.9355 5.06125 5.8975 5.58025C5.09575 5.9815 4.75 6.39025 4.75 6.625C4.75 6.85975 5.09575 7.2685 5.8975 7.66975C6.9355 8.18875 8.40775 8.5 10 8.5Z" fill="currentColor"/> |
| 392 |
</svg> |
| 393 |
|
| 394 |
{{ item.label }} |
| 395 |
</span> |
| 396 |
|
| 397 |
<span v-if="item.region" class="bucket-region"> |
| 398 |
{{ item.region }} |
| 399 |
</span> |
| 400 |
</span> |
| 401 |
|
| 402 |
<span v-if="form.bucket === item.value" class="selected-icon"> |
| 403 |
<svg width="14" height="14" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">> |
| 404 |
<path d="M7 14C3.1339 14 0 10.8661 0 7C0 3.1339 3.1339 0 7 0C10.8661 0 14 3.1339 14 7C14 10.8661 10.8661 14 7 14ZM6.3021 9.8L11.2511 4.8503L10.2613 3.8605L6.3021 7.8204L4.3218 5.8401L3.332 6.8299L6.3021 9.8Z" fill="currentColor"/> |
| 405 |
</svg> |
| 406 |
</span> |
| 407 |
</button> |
| 408 |
</div> |
| 409 |
</div> |
| 410 |
</div> |
| 411 |
</div> |
| 412 |
</Animation> |
| 413 |
</div> |
| 414 |
</li> |
| 415 |
|
| 416 |
<li :class="{ active: bucketAction === 'new' }" @click="bucketAction = 'new'"> |
| 417 |
<div class="w-full"> |
| 418 |
<div class="fct-coverage-selector-content-wrap"> |
| 419 |
<div class="fct-coverage-selector-content"> |
| 420 |
<span class="fct-coverage-selector-title">{{ translate('Create a new bucket') }}</span> |
| 421 |
<span class="fct-coverage-selector-desc">{{ translate('Create and select a fresh bucket using the current credentials.') }}</span> |
| 422 |
</div> |
| 423 |
<div class="fct-coverage-selector-dot-wrap shrink-0"> |
| 424 |
<span class="fct-coverage-selector-dot"></span> |
| 425 |
</div> |
| 426 |
</div> |
| 427 |
|
| 428 |
<Animation :visible="bucketAction === 'new'" accordion :duration="220"> |
| 429 |
<div class="mt-4"> |
| 430 |
<div class="space-y-4"> |
| 431 |
<div class="grid grid-cols-1 gap-4 lg:grid-cols-3"> |
| 432 |
<div class="lg:col-span-2"> |
| 433 |
<label class="mb-2 block text-sm font-medium text-system-dark dark:text-gray-50">{{ translate('Bucket Name') }}</label> |
| 434 |
<el-input v-model="newBucketName" :placeholder="translate('Enter bucket name')" @click.stop /> |
| 435 |
<p v-if="createBucketAttempted && !newBucketName" class="mt-1 text-xs text-red-500"> |
| 436 |
{{ translate('Bucket name not entered.') }} |
| 437 |
</p> |
| 438 |
</div> |
| 439 |
|
| 440 |
<div> |
| 441 |
<label class="mb-2 block text-sm font-medium text-system-dark dark:text-gray-50">{{ translate('Region') }}</label> |
| 442 |
<el-select v-model="newBucketRegion" class="w-full" :placeholder="translate('Select Region')" @click.stop> |
| 443 |
<el-option v-for="region in awsRegions" :key="region.value" :label="region.label" :value="region.value" /> |
| 444 |
</el-select> |
| 445 |
<p v-if="bucketStepAttempted && !hasSelectedRegion(newBucketRegion)" class="mt-1 text-xs text-red-500"> |
| 446 |
{{ translate('Region is required.') }} |
| 447 |
</p> |
| 448 |
</div> |
| 449 |
</div> |
| 450 |
</div> |
| 451 |
</div> |
| 452 |
</Animation> |
| 453 |
</div> |
| 454 |
</li> |
| 455 |
</ul> |
| 456 |
</div> |
| 457 |
|
| 458 |
<div v-show="currentStep === 3" class="fct-step-content-box space-y-4"> |
| 459 |
<h4 class="fct-step-content-box-heading"> |
| 460 |
{{ translate('Review protection settings') }} |
| 461 |
</h4> |
| 462 |
<div class="space-y-4"> |
| 463 |
<div class="config-snippet config-snippet-white"> |
| 464 |
<div class="flex items-start justify-between gap-4"> |
| 465 |
<div class="min-w-0"> |
| 466 |
<div class="flex flex-wrap items-center gap-2"> |
| 467 |
<span class="text-sm font-medium text-system-dark dark:text-gray-50">{{ translate('Block All Public Access') }}</span> |
| 468 |
<span :class="['badge small', form.block_public_access === 'yes' ? 'success' : 'warning']"> |
| 469 |
{{ form.block_public_access === 'yes' ? translate('Recommended') : translate('Review') }} |
| 470 |
</span> |
| 471 |
</div> |
| 472 |
<p class="m-0 mt-2 text-sm text-system-mid dark:text-gray-300"> |
| 473 |
{{ translate('Recommended for private asset delivery.') }} |
| 474 |
</p> |
| 475 |
</div> |
| 476 |
|
| 477 |
<el-switch v-model="form.block_public_access" active-value="yes" inactive-value="no" /> |
| 478 |
</div> |
| 479 |
</div> |
| 480 |
|
| 481 |
<div class="config-snippet config-snippet-white"> |
| 482 |
<div class="flex items-start justify-between gap-4"> |
| 483 |
<div class="min-w-0"> |
| 484 |
<div class="flex flex-wrap items-center gap-2"> |
| 485 |
<span class="text-sm font-medium text-system-dark dark:text-gray-50">{{ translate('Object Ownership') }}</span> |
| 486 |
<span :class="['badge small', form.object_ownership === 'yes' ? 'success' : 'warning']"> |
| 487 |
{{ form.object_ownership === 'yes' ? translate('Recommended') : translate('Review') }} |
| 488 |
</span> |
| 489 |
</div> |
| 490 |
<p class="m-0 mt-2 text-sm text-system-mid dark:text-gray-300"> |
| 491 |
{{ translate('Ensure uploaded files stay under the expected ownership model.') }} |
| 492 |
</p> |
| 493 |
</div> |
| 494 |
|
| 495 |
<el-switch v-model="form.object_ownership" active-value="yes" inactive-value="no" /> |
| 496 |
</div> |
| 497 |
</div> |
| 498 |
</div> |
| 499 |
</div> |
| 500 |
</div> |
| 501 |
</div> |
| 502 |
|
| 503 |
<div class="flex items-center justify-end gap-3"> |
| 504 |
<div class="fct-btn-group"> |
| 505 |
<el-button v-if="editMode" @click="cancelEdit"> |
| 506 |
{{ translate('Cancel') }} |
| 507 |
</el-button> |
| 508 |
|
| 509 |
<el-button v-if="currentStep === 2" @click="currentStep = 1"> |
| 510 |
{{ translate('Back') }} |
| 511 |
</el-button> |
| 512 |
|
| 513 |
<el-button v-if="currentStep === 3" @click="currentStep = 2"> |
| 514 |
{{ translate('Back') }} |
| 515 |
</el-button> |
| 516 |
|
| 517 |
<el-button v-if="currentStep === 1" type="primary" :loading="saving" @click="saveAndContinue"> |
| 518 |
{{ translate('Save & Continue') }} |
| 519 |
</el-button> |
| 520 |
|
| 521 |
<el-button |
| 522 |
v-if="currentStep === 2" |
| 523 |
type="primary" |
| 524 |
:loading="saving" |
| 525 |
@click="advanceFromBucketStep" |
| 526 |
> |
| 527 |
{{ bucketAction === 'new' ? translate('Create Bucket & Continue') : translate('Next') }} |
| 528 |
</el-button> |
| 529 |
|
| 530 |
<el-button v-if="currentStep === 3" type="primary" :loading="saving" @click="saveSecuritySettings"> |
| 531 |
{{ translate('Save Settings') }} |
| 532 |
</el-button> |
| 533 |
</div> |
| 534 |
</div> |
| 535 |
</div> |
| 536 |
</template> |
| 537 |
</div> |
| 538 |
</template> |
| 539 |
|
| 540 |
<script> |
| 541 |
export default { |
| 542 |
components: { |
| 543 |
Animation, |
| 544 |
StepIndicator, |
| 545 |
LoadingButton |
| 546 |
}, |
| 547 |
props: { |
| 548 |
payload: { |
| 549 |
type: Object, |
| 550 |
required: true |
| 551 |
} |
| 552 |
}, |
| 553 |
data() { |
| 554 |
return { |
| 555 |
saving: false, |
| 556 |
fetching: false, |
| 557 |
checking_connection: false, |
| 558 |
bucketLoading: false, |
| 559 |
editMode: false, |
| 560 |
wizardMode: true, |
| 561 |
currentStep: 1, |
| 562 |
bucketAction: 'existing', |
| 563 |
bucketInputMode: 'enter', |
| 564 |
bucketSearchQuery: '', |
| 565 |
newBucketName: '', |
| 566 |
newBucketRegion: 'us-east-1', |
| 567 |
bucketStepAttempted: false, |
| 568 |
createBucketAttempted: false, |
| 569 |
bucketOptions: [], |
| 570 |
connectionError: null, |
| 571 |
form: this.makeForm(this.payload.settings || {}), |
| 572 |
awsRegions: [ |
| 573 |
{ label: 'US East (N. Virginia)', value: 'us-east-1' }, |
| 574 |
{ label: 'US East (Ohio)', value: 'us-east-2' }, |
| 575 |
{ label: 'US West (N. California)', value: 'us-west-1' }, |
| 576 |
{ label: 'US West (Oregon)', value: 'us-west-2' }, |
| 577 |
{ label: 'Canada (Central)', value: 'ca-central-1' }, |
| 578 |
{ label: 'Canada West (Calgary)', value: 'ca-west-1' }, |
| 579 |
{ label: 'Africa (Cape Town)', value: 'af-south-1' }, |
| 580 |
{ label: 'Asia Pacific (Hong Kong)', value: 'ap-east-1' }, |
| 581 |
{ label: 'Asia Pacific (Mumbai)', value: 'ap-south-1' }, |
| 582 |
{ label: 'Asia Pacific (Hyderabad)', value: 'ap-south-2' }, |
| 583 |
{ label: 'Asia Pacific (Tokyo)', value: 'ap-northeast-1' }, |
| 584 |
{ label: 'Asia Pacific (Seoul)', value: 'ap-northeast-2' }, |
| 585 |
{ label: 'Asia Pacific (Osaka)', value: 'ap-northeast-3' }, |
| 586 |
{ label: 'Asia Pacific (Singapore)', value: 'ap-southeast-1' }, |
| 587 |
{ label: 'Asia Pacific (Sydney)', value: 'ap-southeast-2' }, |
| 588 |
{ label: 'Asia Pacific (Jakarta)', value: 'ap-southeast-3' }, |
| 589 |
{ label: 'Asia Pacific (Melbourne)', value: 'ap-southeast-4' }, |
| 590 |
{ label: 'Asia Pacific (Malaysia)', value: 'ap-southeast-5' }, |
| 591 |
{ label: 'Asia Pacific (Thailand)', value: 'ap-southeast-7' }, |
| 592 |
{ label: 'China (Beijing)', value: 'cn-north-1' }, |
| 593 |
{ label: 'China (Ningxia)', value: 'cn-northwest-1' }, |
| 594 |
{ label: 'EU (Frankfurt)', value: 'eu-central-1' }, |
| 595 |
{ label: 'EU (Zurich)', value: 'eu-central-2' }, |
| 596 |
{ label: 'EU (Ireland)', value: 'eu-west-1' }, |
| 597 |
{ label: 'EU (London)', value: 'eu-west-2' }, |
| 598 |
{ label: 'EU (Paris)', value: 'eu-west-3' }, |
| 599 |
{ label: 'EU (Milan)', value: 'eu-south-1' }, |
| 600 |
{ label: 'EU (Spain)', value: 'eu-south-2' }, |
| 601 |
{ label: 'EU (Stockholm)', value: 'eu-north-1' }, |
| 602 |
{ label: 'Israel (Tel Aviv)', value: 'il-central-1' }, |
| 603 |
{ label: 'Middle East (Bahrain)', value: 'me-south-1' }, |
| 604 |
{ label: 'Middle East (UAE)', value: 'me-central-1' }, |
| 605 |
{ label: 'Mexico (Central)', value: 'mx-central-1' }, |
| 606 |
{ label: 'South America (São Paulo)', value: 'sa-east-1' } |
| 607 |
] |
| 608 |
}; |
| 609 |
}, |
| 610 |
computed: { |
| 611 |
showDashboard() { |
| 612 |
return !this.wizardMode && !this.editMode; |
| 613 |
}, |
| 614 |
stepperActive() { |
| 615 |
const currentIndex = this.visibleSteps.findIndex((step) => step.number === this.currentStep); |
| 616 |
return currentIndex >= 0 ? currentIndex : 0; |
| 617 |
}, |
| 618 |
showMissingDefineCredentialsWarning() { |
| 619 |
return this.currentStep === 1 && this.form.define_credentials_missing; |
| 620 |
}, |
| 621 |
missingDefineCredentialsMessage() { |
| 622 |
return this.form.define_credentials_missing_message || this.translate('S3 was configured previously, but the wp-config.php credentials are missing now. Add them again to continue.'); |
| 623 |
}, |
| 624 |
formattedRegion() { |
| 625 |
const region = this.awsRegions.find((item) => item.value === this.form.region); |
| 626 |
return region ? region.label : this.form.region; |
| 627 |
}, |
| 628 |
visibleSteps() { |
| 629 |
const steps = [ |
| 630 |
{ |
| 631 |
number: 1, |
| 632 |
label: this.translate('Credential'), |
| 633 |
description: this.translate('Verify access keys') |
| 634 |
}, |
| 635 |
{ |
| 636 |
number: 2, |
| 637 |
label: this.translate('Bucket'), |
| 638 |
description: this.translate('Choose where files live') |
| 639 |
}, |
| 640 |
{ |
| 641 |
number: 3, |
| 642 |
label: this.translate('Security'), |
| 643 |
description: this.translate('Review protection settings') |
| 644 |
} |
| 645 |
]; |
| 646 |
|
| 647 |
// Keep the credential step reachable in edit mode: dropping it strands |
| 648 |
// anyone whose stored keys are missing or rejected, leaving Reset as the |
| 649 |
// only way back to the key fields. |
| 650 |
return steps; |
| 651 |
}, |
| 652 |
connectionMethodLabel() { |
| 653 |
if (this.form.auth_method === 'define') { |
| 654 |
return this.translate('Defined mode'); |
| 655 |
} |
| 656 |
|
| 657 |
return this.translate('Database mode'); |
| 658 |
}, |
| 659 |
securityRecommendationLabel() { |
| 660 |
return this.form.block_public_access === 'yes' && this.form.object_ownership === 'yes' |
| 661 |
? this.translate('Recommended') |
| 662 |
: this.translate('Needs review'); |
| 663 |
}, |
| 664 |
bucketConsoleUrl() { |
| 665 |
if (!this.form.bucket) { |
| 666 |
return ''; |
| 667 |
} |
| 668 |
|
| 669 |
return `https://console.aws.amazon.com/s3/buckets/${this.form.bucket}/?region=${this.form.region || 'us-east-1'}`; |
| 670 |
}, |
| 671 |
filteredBucketOptions() { |
| 672 |
const query = (this.bucketSearchQuery || '').trim().toLowerCase(); |
| 673 |
|
| 674 |
if (!query) { |
| 675 |
return this.bucketOptions; |
| 676 |
} |
| 677 |
|
| 678 |
return this.bucketOptions.filter((item) => { |
| 679 |
return [item.label, item.value, item.region].some((value) => { |
| 680 |
return String(value || '').toLowerCase().includes(query); |
| 681 |
}); |
| 682 |
}); |
| 683 |
}, |
| 684 |
bucketListClasses() { |
| 685 |
const classes = []; |
| 686 |
|
| 687 |
if (this.filteredBucketOptions.length > 5) { |
| 688 |
classes.push('max-h-[16rem]', 'overflow-y-auto'); |
| 689 |
} |
| 690 |
|
| 691 |
return classes; |
| 692 |
}, |
| 693 |
wpConfigSnippet() { |
| 694 |
return `define( 'FCT_S3_ACCESS_KEY', '********************' ); |
| 695 |
define( 'FCT_S3_SECRET_KEY', '****************************************' );`; |
| 696 |
} |
| 697 |
}, |
| 698 |
watch: { |
| 699 |
payload: { |
| 700 |
deep: true, |
| 701 |
handler(newPayload) { |
| 702 |
this.applyServerSettings(newPayload.settings || {}); |
| 703 |
} |
| 704 |
}, |
| 705 |
newBucketName(val) { |
| 706 |
const formatted = (val || '').toLowerCase().replace(/[^a-z0-9.-]/g, '-').replace(/-+/g, '-'); |
| 707 |
if (formatted !== val) { |
| 708 |
this.newBucketName = formatted; |
| 709 |
} |
| 710 |
}, |
| 711 |
bucketAction(newValue) { |
| 712 |
if (newValue === 'existing') { |
| 713 |
this.bucketSearchQuery = ''; |
| 714 |
this.ensureBucketOptionsLoaded(); |
| 715 |
} |
| 716 |
}, |
| 717 |
bucketInputMode() { |
| 718 |
this.form.bucket = ''; |
| 719 |
}, |
| 720 |
currentStep(newValue) { |
| 721 |
if (newValue === 2 && this.bucketAction === 'existing') { |
| 722 |
this.ensureBucketOptionsLoaded(); |
| 723 |
} |
| 724 |
} |
| 725 |
}, |
| 726 |
mounted() { |
| 727 |
this.applyServerSettings(this.payload.settings || {}); |
| 728 |
}, |
| 729 |
methods: { |
| 730 |
translate, |
| 731 |
isStepAvailable(step) { |
| 732 |
return this.editMode || step < this.currentStep || (this.form.bucket && step <= 3); |
| 733 |
}, |
| 734 |
defaults() { |
| 735 |
return { |
| 736 |
is_active: 'no', |
| 737 |
auth_method: 'define', |
| 738 |
access_key: '', |
| 739 |
secret_key: '', |
| 740 |
bucket: '', |
| 741 |
region: 'us-east-1', |
| 742 |
buckets: [], |
| 743 |
defined_in_wp_config: false, |
| 744 |
define_credentials_missing: false, |
| 745 |
define_credentials_missing_message: '', |
| 746 |
block_public_access: 'yes', |
| 747 |
object_ownership: 'yes', |
| 748 |
connection_error: '' |
| 749 |
}; |
| 750 |
}, |
| 751 |
normalizeSettings(settings = {}) { |
| 752 |
const normalized = { |
| 753 |
...this.defaults(), |
| 754 |
...settings |
| 755 |
}; |
| 756 |
|
| 757 |
normalized.defined_in_wp_config = !!normalized.defined_in_wp_config; |
| 758 |
normalized.define_credentials_missing = !!normalized.define_credentials_missing; |
| 759 |
normalized.block_public_access = normalized.block_public_access === true || normalized.block_public_access === 'yes' ? 'yes' : 'no'; |
| 760 |
normalized.object_ownership = normalized.object_ownership === true || normalized.object_ownership === 'yes' ? 'yes' : 'no'; |
| 761 |
|
| 762 |
return normalized; |
| 763 |
}, |
| 764 |
makeForm(settings) { |
| 765 |
return this.normalizeSettings(settings); |
| 766 |
}, |
| 767 |
applyServerSettings(settings = {}, options = {}) { |
| 768 |
const { preserveWizardState = false } = options; |
| 769 |
const normalized = this.normalizeSettings(settings); |
| 770 |
|
| 771 |
if (!Object.prototype.hasOwnProperty.call(settings, 'access_key') && this.form?.access_key) { |
| 772 |
normalized.access_key = this.form.access_key; |
| 773 |
} |
| 774 |
|
| 775 |
if (!Object.prototype.hasOwnProperty.call(settings, 'secret_key') && this.form?.secret_key) { |
| 776 |
normalized.secret_key = this.form.secret_key; |
| 777 |
} |
| 778 |
|
| 779 |
this.form = { |
| 780 |
...this.form, |
| 781 |
...normalized |
| 782 |
}; |
| 783 |
|
| 784 |
if (this.form.define_credentials_missing) { |
| 785 |
this.connectionError = null; |
| 786 |
this.bucketOptions = []; |
| 787 |
this.wizardMode = true; |
| 788 |
this.currentStep = 1; |
| 789 |
return; |
| 790 |
} |
| 791 |
|
| 792 |
this.connectionError = settings.connection_error || null; |
| 793 |
|
| 794 |
if (preserveWizardState) { |
| 795 |
return; |
| 796 |
} |
| 797 |
|
| 798 |
this.wizardMode = !this.form.bucket; |
| 799 |
this.currentStep = this.form.bucket ? 2 : 1; |
| 800 |
}, |
| 801 |
mergeResponseSettings(response, options = {}) { |
| 802 |
const serverSettings = response?.data?.data || response?.data || response?.settings || {}; |
| 803 |
this.applyServerSettings(serverSettings, options); |
| 804 |
return serverSettings; |
| 805 |
}, |
| 806 |
copyCode() { |
| 807 |
if (!navigator?.clipboard?.writeText) { |
| 808 |
Notify.error(this.translate('Clipboard API is not available.')); |
| 809 |
return; |
| 810 |
} |
| 811 |
|
| 812 |
navigator.clipboard.writeText(this.wpConfigSnippet).then(() => { |
| 813 |
Notify.success(this.translate('Code copied to clipboard!')); |
| 814 |
}).catch(() => { |
| 815 |
Notify.error(this.translate('Failed to copy code.')); |
| 816 |
}); |
| 817 |
}, |
| 818 |
getDriver() { |
| 819 |
return this.payload.driver || 's3'; |
| 820 |
}, |
| 821 |
getSaveEndpoint() { |
| 822 |
return this.payload.save_endpoint || 'settings/storage-drivers'; |
| 823 |
}, |
| 824 |
getCreateBucketEndpoint() { |
| 825 |
return this.payload.create_bucket_endpoint || 'settings/storage-drivers/create-bucket'; |
| 826 |
}, |
| 827 |
getBucketListEndpoint() { |
| 828 |
return this.payload.bucket_list_endpoint || 'settings/storage-drivers/bucket-list'; |
| 829 |
}, |
| 830 |
getVerifyEndpoint() { |
| 831 |
return this.payload.connection_verify_endpoint || 'settings/storage-drivers/verify-info'; |
| 832 |
}, |
| 833 |
getResetEndpoint() { |
| 834 |
return this.payload.reset_endpoint || 'settings/storage-drivers/reset'; |
| 835 |
}, |
| 836 |
enableEditMode() { |
| 837 |
this.editMode = true; |
| 838 |
this.bucketAction = 'existing'; |
| 839 |
this.bucketInputMode = 'enter'; |
| 840 |
this.currentStep = this.form.bucket ? 2 : 1; |
| 841 |
this.bucketSearchQuery = ''; |
| 842 |
this.connectionError = null; |
| 843 |
}, |
| 844 |
cancelEdit() { |
| 845 |
this.editMode = false; |
| 846 |
this.refreshSettings(); |
| 847 |
}, |
| 848 |
goToStep(step) { |
| 849 |
if (this.editMode || step < this.currentStep || (this.form.bucket && step <= 3)) { |
| 850 |
this.currentStep = step; |
| 851 |
} |
| 852 |
}, |
| 853 |
handleStepClick(localIndex) { |
| 854 |
const step = this.visibleSteps[localIndex - 1]; |
| 855 |
if (step) { |
| 856 |
this.goToStep(step.number); |
| 857 |
} |
| 858 |
}, |
| 859 |
refreshSettings(showLoading = false) { |
| 860 |
if (showLoading) { |
| 861 |
this.fetching = true; |
| 862 |
} |
| 863 |
|
| 864 |
return Rest.get('settings/storage-drivers/' + this.getDriver()).then((response) => { |
| 865 |
this.applyServerSettings(response.settings || {}); |
| 866 |
}).finally(() => { |
| 867 |
if (showLoading) { |
| 868 |
this.fetching = false; |
| 869 |
} |
| 870 |
}); |
| 871 |
}, |
| 872 |
checkConnection() { |
| 873 |
this.checking_connection = true; |
| 874 |
|
| 875 |
Rest.post(this.getVerifyEndpoint(), { |
| 876 |
driver: this.getDriver(), |
| 877 |
settings: this.buildSettingsPayload() |
| 878 |
}).then((response) => { |
| 879 |
this.connectionError = null; |
| 880 |
Notify.success(response?.message || this.translate('Connection verified successfully.')); |
| 881 |
}).catch((errors) => { |
| 882 |
const message = errors?.data?.message || this.translate('Connection failed. Please check settings.'); |
| 883 |
this.connectionError = message; |
| 884 |
Notify.error(message); |
| 885 |
}).finally(() => { |
| 886 |
this.checking_connection = false; |
| 887 |
}); |
| 888 |
}, |
| 889 |
handleSwitchChange() { |
| 890 |
if (this.form.is_active === 'yes') { |
| 891 |
this.activateDriver().catch(() => { |
| 892 |
this.form.is_active = 'no'; |
| 893 |
}); |
| 894 |
} else { |
| 895 |
this.deactivateDriver().catch(() => { |
| 896 |
this.form.is_active = 'yes'; |
| 897 |
}); |
| 898 |
} |
| 899 |
}, |
| 900 |
activateDriver() { |
| 901 |
this.saving = true; |
| 902 |
return Rest.post(this.getSaveEndpoint(), { |
| 903 |
driver: this.getDriver(), |
| 904 |
settings: this.buildSettingsPayload({ |
| 905 |
is_active: 'yes' |
| 906 |
}) |
| 907 |
}).then((response) => { |
| 908 |
this.mergeResponseSettings(response); |
| 909 |
Notify.success(this.translate('S3 activated successfully.')); |
| 910 |
}).catch((errors) => { |
| 911 |
const message = errors?.data?.message || this.translate('Failed to activate driver.'); |
| 912 |
Notify.error(message); |
| 913 |
throw errors; |
| 914 |
}).finally(() => { |
| 915 |
this.saving = false; |
| 916 |
}); |
| 917 |
}, |
| 918 |
deactivateDriver() { |
| 919 |
this.saving = true; |
| 920 |
return Rest.post(this.getSaveEndpoint(), { |
| 921 |
driver: this.getDriver(), |
| 922 |
settings: this.buildSettingsPayload({ |
| 923 |
is_active: 'no', |
| 924 |
preserve_settings: true |
| 925 |
}) |
| 926 |
}).then(() => { |
| 927 |
Notify.success(this.translate('S3 deactivated successfully.')); |
| 928 |
}).catch((errors) => { |
| 929 |
const message = errors?.data?.message || this.translate('Failed to deactivate driver.'); |
| 930 |
Notify.error(message); |
| 931 |
throw errors; |
| 932 |
}).finally(() => { |
| 933 |
this.saving = false; |
| 934 |
}); |
| 935 |
}, |
| 936 |
resetSettings() { |
| 937 |
this.saving = true; |
| 938 |
Rest.post(this.getResetEndpoint(), { |
| 939 |
driver: this.getDriver() |
| 940 |
}).then((response) => { |
| 941 |
this.applyServerSettings(response?.data?.data || {}); |
| 942 |
this.form = this.makeForm(response?.data?.data || {}); |
| 943 |
this.currentStep = 1; |
| 944 |
this.editMode = false; |
| 945 |
this.wizardMode = true; |
| 946 |
this.bucketOptions = []; |
| 947 |
Notify.success(this.translate('S3 settings reset successfully.')); |
| 948 |
}).catch((errors) => { |
| 949 |
const message = errors?.data?.message || this.translate('Failed to reset settings.'); |
| 950 |
Notify.error(message); |
| 951 |
}).finally(() => { |
| 952 |
this.saving = false; |
| 953 |
}); |
| 954 |
}, |
| 955 |
fetchBuckets(query) { |
| 956 |
this.bucketLoading = true; |
| 957 |
Rest.post(this.getBucketListEndpoint(), { |
| 958 |
driver: this.getDriver(), |
| 959 |
query: query, |
| 960 |
settings: this.buildSettingsPayload() |
| 961 |
}).then((response) => { |
| 962 |
this.bucketOptions = response.options || []; |
| 963 |
}).catch((errors) => { |
| 964 |
this.bucketOptions = []; |
| 965 |
|
| 966 |
const message = errors?.data?.message || this.translate('Failed to load buckets.'); |
| 967 |
Notify.error(message); |
| 968 |
}).finally(() => { |
| 969 |
this.bucketLoading = false; |
| 970 |
}); |
| 971 |
}, |
| 972 |
ensureBucketOptionsLoaded() { |
| 973 |
if (!this.bucketOptions.length && !this.bucketLoading) { |
| 974 |
this.fetchBuckets(''); |
| 975 |
} |
| 976 |
}, |
| 977 |
refreshBucketList() { |
| 978 |
this.fetchBuckets(''); |
| 979 |
}, |
| 980 |
handleBucketSearch() { |
| 981 |
this.bucketSearchQuery = (this.bucketSearchQuery || '').trim(); |
| 982 |
}, |
| 983 |
hasSelectedRegion(region) { |
| 984 |
return !!String(region || '').trim(); |
| 985 |
}, |
| 986 |
resetCreateBucketState(options = {}) { |
| 987 |
const { clearName = false } = options; |
| 988 |
|
| 989 |
this.bucketStepAttempted = false; |
| 990 |
this.createBucketAttempted = false; |
| 991 |
|
| 992 |
if (clearName) { |
| 993 |
this.newBucketName = ''; |
| 994 |
} |
| 995 |
}, |
| 996 |
buildSettingsPayload(extra = {}) { |
| 997 |
const settings = { |
| 998 |
...this.form, |
| 999 |
...extra |
| 1000 |
}; |
| 1001 |
|
| 1002 |
delete settings.create_new_bucket; |
| 1003 |
delete settings.new_bucket_name; |
| 1004 |
delete settings.new_bucket_region; |
| 1005 |
|
| 1006 |
// Never post blank credentials: the server hides them from the settings |
| 1007 |
// response, so an untouched field must read as "not submitted" and keep |
| 1008 |
// the stored value instead of overwriting it with an empty string. |
| 1009 |
['access_key', 'secret_key'].forEach((key) => { |
| 1010 |
if (!settings[key]) { |
| 1011 |
delete settings[key]; |
| 1012 |
} |
| 1013 |
}); |
| 1014 |
|
| 1015 |
return settings; |
| 1016 |
}, |
| 1017 |
selectBucket(item) { |
| 1018 |
this.form.bucket = item.value; |
| 1019 |
if (item.region) { |
| 1020 |
this.form.region = item.region; |
| 1021 |
} |
| 1022 |
}, |
| 1023 |
advanceFromBucketStep() { |
| 1024 |
this.bucketStepAttempted = true; |
| 1025 |
|
| 1026 |
if (this.bucketAction === 'new') { |
| 1027 |
if (!this.hasSelectedRegion(this.newBucketRegion)) { |
| 1028 |
Notify.error(this.translate('Please select a region before continuing.')); |
| 1029 |
return; |
| 1030 |
} |
| 1031 |
this.createNewBucket(); |
| 1032 |
return; |
| 1033 |
} |
| 1034 |
|
| 1035 |
this.saveBucketSettings(); |
| 1036 |
}, |
| 1037 |
saveAndContinue() { |
| 1038 |
this.saving = true; |
| 1039 |
Rest.post(this.getSaveEndpoint(), { |
| 1040 |
driver: this.getDriver(), |
| 1041 |
settings: this.buildSettingsPayload({ |
| 1042 |
is_active: 'no', |
| 1043 |
preserve_settings: true, |
| 1044 |
verify_credentials: true, |
| 1045 |
verify_only_credentials: true |
| 1046 |
}) |
| 1047 |
}).then((response) => { |
| 1048 |
this.mergeResponseSettings(response); |
| 1049 |
this.currentStep = 2; |
| 1050 |
this.bucketOptions = []; |
| 1051 |
this.bucketSearchQuery = ''; |
| 1052 |
this.ensureBucketOptionsLoaded(); |
| 1053 |
Notify.success(this.translate('Credentials saved. Proceeding to bucket selection.')); |
| 1054 |
}).catch((errors) => { |
| 1055 |
const message = errors?.data?.message || this.translate('Failed to save credentials.'); |
| 1056 |
if (errors.status_code === '422' && Notify.validationErrors) { |
| 1057 |
Notify.validationErrors(errors); |
| 1058 |
} else { |
| 1059 |
Notify.error(message); |
| 1060 |
} |
| 1061 |
}).finally(() => { |
| 1062 |
this.saving = false; |
| 1063 |
}); |
| 1064 |
}, |
| 1065 |
createNewBucket() { |
| 1066 |
this.createBucketAttempted = true; |
| 1067 |
|
| 1068 |
if (!this.newBucketName) { |
| 1069 |
return; |
| 1070 |
} |
| 1071 |
|
| 1072 |
this.saving = true; |
| 1073 |
Rest.post(this.getCreateBucketEndpoint(), { |
| 1074 |
driver: this.getDriver(), |
| 1075 |
settings: { |
| 1076 |
...this.buildSettingsPayload(), |
| 1077 |
create_new_bucket: 'yes', |
| 1078 |
new_bucket_name: this.newBucketName, |
| 1079 |
new_bucket_region: this.newBucketRegion |
| 1080 |
} |
| 1081 |
}).then((response) => { |
| 1082 |
const bucketData = response?.data?.data || response?.data || {}; |
| 1083 |
this.form.bucket = bucketData.bucket || this.newBucketName; |
| 1084 |
this.form.region = bucketData.region || this.newBucketRegion; |
| 1085 |
this.form.connection_error = ''; |
| 1086 |
|
| 1087 |
if (bucketData.block_public_access) { |
| 1088 |
this.form.block_public_access = bucketData.block_public_access; |
| 1089 |
} |
| 1090 |
|
| 1091 |
if (bucketData.object_ownership) { |
| 1092 |
this.form.object_ownership = bucketData.object_ownership; |
| 1093 |
} |
| 1094 |
|
| 1095 |
this.bucketAction = 'existing'; |
| 1096 |
this.bucketSearchQuery = ''; |
| 1097 |
this.resetCreateBucketState({ |
| 1098 |
clearName: true |
| 1099 |
}); |
| 1100 |
this.currentStep = 3; |
| 1101 |
Notify.success(this.translate('Bucket created and selected successfully.')); |
| 1102 |
}).catch((errors) => { |
| 1103 |
const message = errors?.data?.message || this.translate('Failed to create bucket.'); |
| 1104 |
if (errors.status_code === '422' && Notify.validationErrors) { |
| 1105 |
Notify.validationErrors(errors); |
| 1106 |
} else { |
| 1107 |
Notify.error(message); |
| 1108 |
} |
| 1109 |
}).finally(() => { |
| 1110 |
this.saving = false; |
| 1111 |
}); |
| 1112 |
}, |
| 1113 |
saveBucketSettings() { |
| 1114 |
this.form.bucket = (this.form.bucket || '').trim(); |
| 1115 |
|
| 1116 |
if (!this.form.bucket) { |
| 1117 |
Notify.error(this.translate('Please select or enter a bucket before continuing.')); |
| 1118 |
return; |
| 1119 |
} |
| 1120 |
|
| 1121 |
this.saving = true; |
| 1122 |
Rest.post(this.getSaveEndpoint(), { |
| 1123 |
driver: this.getDriver(), |
| 1124 |
settings: this.buildSettingsPayload({ |
| 1125 |
is_active: 'no', |
| 1126 |
preserve_settings: true, |
| 1127 |
check_bucket_exists: 'yes' |
| 1128 |
}) |
| 1129 |
}).then((response) => { |
| 1130 |
this.mergeResponseSettings(response, { |
| 1131 |
preserveWizardState: true |
| 1132 |
}); |
| 1133 |
this.resetCreateBucketState(); |
| 1134 |
this.currentStep = 3; |
| 1135 |
Notify.success(this.translate('Bucket settings saved.')); |
| 1136 |
}).catch((errors) => { |
| 1137 |
const message = errors?.data?.message || this.translate('Failed to save bucket settings.'); |
| 1138 |
if (errors.status_code === '422' && Notify.validationErrors) { |
| 1139 |
Notify.validationErrors(errors); |
| 1140 |
} else { |
| 1141 |
Notify.error(message); |
| 1142 |
} |
| 1143 |
}).finally(() => { |
| 1144 |
this.saving = false; |
| 1145 |
}); |
| 1146 |
}, |
| 1147 |
saveSecuritySettings() { |
| 1148 |
this.saving = true; |
| 1149 |
Rest.post(this.getSaveEndpoint(), { |
| 1150 |
driver: this.getDriver(), |
| 1151 |
settings: this.buildSettingsPayload({ |
| 1152 |
is_active: 'yes' |
| 1153 |
}) |
| 1154 |
}).then((response) => { |
| 1155 |
this.mergeResponseSettings(response); |
| 1156 |
this.wizardMode = false; |
| 1157 |
this.editMode = false; |
| 1158 |
this.resetCreateBucketState({ |
| 1159 |
clearName: true |
| 1160 |
}); |
| 1161 |
Notify.success(this.translate('S3 settings saved successfully.')); |
| 1162 |
}).catch((errors) => { |
| 1163 |
const message = errors?.data?.message || this.translate('Failed to save security settings.'); |
| 1164 |
Notify.error(message); |
| 1165 |
}).finally(() => { |
| 1166 |
this.saving = false; |
| 1167 |
}); |
| 1168 |
} |
| 1169 |
} |
| 1170 |
}; |
| 1171 |
</script> |
| 1172 |
|