@plutocms/supabase 0.14.1 → 0.15.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.prettierignore +6 -0
- package/CHANGELOG.md +23 -0
- package/app/components/access/DeleteRoleModal.vue +17 -4
- package/app/components/access/EditRolesSlideover.vue +11 -5
- package/app/components/access/InviteModal.vue +44 -19
- package/app/components/access/LinkField.vue +14 -3
- package/app/components/access/LinkModal.vue +5 -1
- package/app/components/access/RevokeModal.vue +17 -4
- package/app/components/access/RoleSlideover.vue +32 -11
- package/app/components/mail/AuthEmailsPanel.vue +87 -20
- package/app/components/migrations/MigrationsBanner.vue +5 -4
- package/app/composables/migrations.ts +13 -6
- package/app/pages/admin/(auth)/accept-invite.vue +21 -5
- package/app/pages/admin/(auth)/forgot-password.vue +11 -4
- package/app/pages/admin/(auth)/signup.vue +10 -9
- package/app/pages/admin/(auth)/update-password.vue +8 -3
- package/app/pages/admin/migrations.vue +8 -3
- package/app/pages/admin/settings/roles.vue +58 -15
- package/app/pages/admin/settings/users.vue +105 -30
- package/app/pages/admin/setup.vue +26 -7
- package/app/plugins/pluto-extension.ts +21 -8
- package/app/utils/access-ui.ts +29 -8
- package/app/utils/api-failure.ts +3 -1
- package/app/utils/auth-driver.ts +1 -3
- package/app/utils/auth-error.ts +1 -1
- package/app/utils/invite-link.ts +3 -3
- package/app/utils/supabase-project.ts +8 -3
- package/modules/pluto-migrations.ts +38 -74
- package/package.json +5 -3
- package/server/api/_pluto/supabase/access/users/[id]/resend.post.ts +15 -5
- package/server/api/_pluto/supabase/access/users/[id]/roles.put.ts +4 -1
- package/server/api/_pluto/supabase/access/users/invite-link.post.ts +12 -3
- package/server/api/_pluto/supabase/access/users/invite.post.ts +7 -2
- package/server/api/_pluto/supabase/auth/signup.post.ts +5 -1
- package/server/api/_pluto/supabase/me/capabilities.get.ts +4 -1
- package/server/api/_pluto/supabase/migrations/run.post.ts +9 -2
- package/server/api/_pluto/supabase/setup/index.post.ts +4 -1
- package/server/utils/access-errors.ts +2 -1
- package/server/utils/access-invite.ts +1 -1
- package/server/utils/access-mail.ts +72 -28
- package/server/utils/capability-guard.ts +5 -2
- package/server/utils/content-adapter.ts +75 -24
- package/server/utils/migrations.ts +0 -0
- package/server/utils/pending-migrations.ts +0 -0
- package/server/utils/service-client.ts +3 -1
- package/server/utils/sign-in-link.ts +25 -9
- package/server/utils/typegen.ts +6 -4
- package/server/utils/validate.ts +4 -1
- package/shared/types/migrations.d.ts +3 -4
- package/shared/types/supabase.ts +62 -65
- package/tailwind.lint.css +9 -0
|
@@ -25,8 +25,14 @@ async function submitForm() {
|
|
|
25
25
|
|
|
26
26
|
<template>
|
|
27
27
|
<PlutoAdminAuthCard
|
|
28
|
-
:title="
|
|
29
|
-
|
|
28
|
+
:title="
|
|
29
|
+
isEmailSent
|
|
30
|
+
? t('supabase.auth.forgot.sentTitle')
|
|
31
|
+
: t('supabase.auth.forgot.title')
|
|
32
|
+
"
|
|
33
|
+
:description="
|
|
34
|
+
isEmailSent ? undefined : t('supabase.auth.forgot.description')
|
|
35
|
+
"
|
|
30
36
|
>
|
|
31
37
|
<div v-if="isEmailSent" class="space-y-6 py-10">
|
|
32
38
|
<div class="text-center space-y-4">
|
|
@@ -37,7 +43,9 @@ async function submitForm() {
|
|
|
37
43
|
</hgroup>
|
|
38
44
|
|
|
39
45
|
<p>
|
|
40
|
-
<SupabaseMessageParts
|
|
46
|
+
<SupabaseMessageParts
|
|
47
|
+
:message="t('supabase.auth.forgot.sentDescription')"
|
|
48
|
+
>
|
|
41
49
|
<template #email>
|
|
42
50
|
<strong>{{ email }}</strong>
|
|
43
51
|
</template>
|
|
@@ -54,7 +62,6 @@ async function submitForm() {
|
|
|
54
62
|
|
|
55
63
|
<form v-else @submit.prevent="submitForm">
|
|
56
64
|
<div class="flex flex-col gap-y-6">
|
|
57
|
-
|
|
58
65
|
<UFormField :label="t('supabase.auth.fields.email')">
|
|
59
66
|
<UInput
|
|
60
67
|
v-model="email"
|
|
@@ -62,8 +62,10 @@ async function submitForm() {
|
|
|
62
62
|
description: resolveAuthErrorText(
|
|
63
63
|
{
|
|
64
64
|
code: failureData(error)?.code,
|
|
65
|
-
message:
|
|
66
|
-
|
|
65
|
+
message:
|
|
66
|
+
(error as { data?: { statusMessage?: string } }).data
|
|
67
|
+
?.statusMessage ??
|
|
68
|
+
errorText(error, 'supabase.auth.errors.unknown'),
|
|
67
69
|
},
|
|
68
70
|
{ t, te }
|
|
69
71
|
),
|
|
@@ -78,7 +80,11 @@ async function submitForm() {
|
|
|
78
80
|
|
|
79
81
|
<template>
|
|
80
82
|
<PlutoAdminAuthCard
|
|
81
|
-
:title="
|
|
83
|
+
:title="
|
|
84
|
+
isEmailVerificationMessageVisible
|
|
85
|
+
? t('supabase.auth.signup.almostThere')
|
|
86
|
+
: t('supabase.auth.signup.title')
|
|
87
|
+
"
|
|
82
88
|
>
|
|
83
89
|
<div v-if="isEmailVerificationMessageVisible" class="space-y-6 py-10">
|
|
84
90
|
<div class="text-center space-y-4">
|
|
@@ -98,11 +104,7 @@ async function submitForm() {
|
|
|
98
104
|
</template>
|
|
99
105
|
</UAlert>
|
|
100
106
|
|
|
101
|
-
<UAlert
|
|
102
|
-
icon="lucide:alert-triangle"
|
|
103
|
-
color="warning"
|
|
104
|
-
variant="outline"
|
|
105
|
-
>
|
|
107
|
+
<UAlert icon="lucide:alert-triangle" color="warning" variant="outline">
|
|
106
108
|
<template #description>
|
|
107
109
|
{{ t('supabase.auth.signup.checkSpam') }}
|
|
108
110
|
</template>
|
|
@@ -112,7 +114,6 @@ async function submitForm() {
|
|
|
112
114
|
|
|
113
115
|
<form v-else @submit.prevent="submitForm">
|
|
114
116
|
<div class="flex flex-col gap-y-6">
|
|
115
|
-
|
|
116
117
|
<UFormField :label="t('supabase.auth.fields.email')" required>
|
|
117
118
|
<UInput
|
|
118
119
|
v-model="form.email"
|
|
@@ -51,8 +51,14 @@ async function submitForm() {
|
|
|
51
51
|
|
|
52
52
|
<template>
|
|
53
53
|
<PlutoAdminAuthCard
|
|
54
|
-
:title="
|
|
55
|
-
|
|
54
|
+
:title="
|
|
55
|
+
isPasswordUpdated
|
|
56
|
+
? t('supabase.auth.update.doneTitle')
|
|
57
|
+
: t('supabase.auth.update.title')
|
|
58
|
+
"
|
|
59
|
+
:description="
|
|
60
|
+
isPasswordUpdated ? undefined : t('supabase.auth.update.description')
|
|
61
|
+
"
|
|
56
62
|
>
|
|
57
63
|
<div v-if="isPasswordUpdated" class="space-y-6 py-10">
|
|
58
64
|
<div class="text-center space-y-4">
|
|
@@ -76,7 +82,6 @@ async function submitForm() {
|
|
|
76
82
|
|
|
77
83
|
<form v-else @submit.prevent="submitForm">
|
|
78
84
|
<div class="flex flex-col gap-y-6">
|
|
79
|
-
|
|
80
85
|
<UFormField :label="t('supabase.auth.update.newPassword')">
|
|
81
86
|
<UInput
|
|
82
87
|
v-model="form.password"
|
|
@@ -189,7 +189,9 @@ async function applyMigrations(useForm: boolean) {
|
|
|
189
189
|
|
|
190
190
|
<template>
|
|
191
191
|
<AdminView>
|
|
192
|
-
<h1 class="text-3xl font-bold lg:text-4xl">
|
|
192
|
+
<h1 class="text-3xl font-bold lg:text-4xl">
|
|
193
|
+
{{ t('supabase.migrations.title') }}
|
|
194
|
+
</h1>
|
|
193
195
|
|
|
194
196
|
<UAlert
|
|
195
197
|
v-if="isReconnecting"
|
|
@@ -266,7 +268,10 @@ async function applyMigrations(useForm: boolean) {
|
|
|
266
268
|
class="flex flex-col gap-y-4"
|
|
267
269
|
@submit="applyMigrations(true)"
|
|
268
270
|
>
|
|
269
|
-
<UFormField
|
|
271
|
+
<UFormField
|
|
272
|
+
:label="t('supabase.connection.connectionString')"
|
|
273
|
+
required
|
|
274
|
+
>
|
|
270
275
|
<UInput
|
|
271
276
|
v-model="connectionForm.connectionString"
|
|
272
277
|
:disabled="isRunning"
|
|
@@ -388,7 +393,7 @@ async function applyMigrations(useForm: boolean) {
|
|
|
388
393
|
:key="`${result.layerName}/${result.migrationName}`"
|
|
389
394
|
>
|
|
390
395
|
<span class="font-mono"
|
|
391
|
-
|
|
396
|
+
>{{ result.layerName }}/{{ result.migrationName }}</span
|
|
392
397
|
>
|
|
393
398
|
— {{ statusText(result.status) }}
|
|
394
399
|
<span v-if="result.error" class="text-error">
|
|
@@ -7,15 +7,20 @@ import type { AccessRole } from '../../../../shared/types/access'
|
|
|
7
7
|
import { ADMIN_ROLE_KEY } from '../../../utils/access-ui'
|
|
8
8
|
|
|
9
9
|
// The slideover and the modal load on demand. They stay out of the page chunk.
|
|
10
|
-
const RoleSlideover = defineAsyncComponent(
|
|
11
|
-
|
|
10
|
+
const RoleSlideover = defineAsyncComponent(
|
|
11
|
+
() => import('../../../components/access/RoleSlideover.vue')
|
|
12
|
+
)
|
|
13
|
+
const DeleteRoleModal = defineAsyncComponent(
|
|
14
|
+
() => import('../../../components/access/DeleteRoleModal.vue')
|
|
15
|
+
)
|
|
12
16
|
|
|
13
17
|
const { t } = usePlutoI18n()
|
|
14
18
|
const overlay = useOverlay()
|
|
15
19
|
const errorText = usePlutoErrorText()
|
|
16
20
|
|
|
17
21
|
useHead({
|
|
18
|
-
title: () =>
|
|
22
|
+
title: () =>
|
|
23
|
+
`${t('supabase.access.roles.title')} - ${t('pluto.pages.settings')}`,
|
|
19
24
|
})
|
|
20
25
|
|
|
21
26
|
const roles = ref<AccessRole[]>([])
|
|
@@ -29,7 +34,9 @@ async function load() {
|
|
|
29
34
|
}
|
|
30
35
|
|
|
31
36
|
try {
|
|
32
|
-
const response = await $fetch<{ roles: AccessRole[] }>(
|
|
37
|
+
const response = await $fetch<{ roles: AccessRole[] }>(
|
|
38
|
+
'/api/_pluto/supabase/access/roles'
|
|
39
|
+
)
|
|
33
40
|
|
|
34
41
|
roles.value = response.roles
|
|
35
42
|
status.value = 'ready'
|
|
@@ -77,7 +84,9 @@ function menuItems(role: AccessRole): DropdownMenuItem[][] {
|
|
|
77
84
|
const readOnly = role.key === ADMIN_ROLE_KEY
|
|
78
85
|
const main: DropdownMenuItem[] = [
|
|
79
86
|
{
|
|
80
|
-
label: readOnly
|
|
87
|
+
label: readOnly
|
|
88
|
+
? t('supabase.access.roles.menu.view')
|
|
89
|
+
: t('supabase.access.roles.menu.edit'),
|
|
81
90
|
icon: readOnly ? 'lucide:eye' : 'lucide:pencil',
|
|
82
91
|
onSelect: () => openRole(role),
|
|
83
92
|
},
|
|
@@ -106,8 +115,12 @@ function menuItems(role: AccessRole): DropdownMenuItem[][] {
|
|
|
106
115
|
<div class="flex flex-col gap-y-6">
|
|
107
116
|
<div class="flex flex-wrap items-start justify-between gap-4">
|
|
108
117
|
<div>
|
|
109
|
-
<h2 class="text-xl font-semibold">
|
|
110
|
-
|
|
118
|
+
<h2 class="text-xl font-semibold">
|
|
119
|
+
{{ t('supabase.access.roles.title') }}
|
|
120
|
+
</h2>
|
|
121
|
+
<p class="text-muted text-sm">
|
|
122
|
+
{{ t('supabase.access.roles.description') }}
|
|
123
|
+
</p>
|
|
111
124
|
</div>
|
|
112
125
|
|
|
113
126
|
<UButton icon="lucide:plus" @click="openRole(null)">
|
|
@@ -116,7 +129,11 @@ function menuItems(role: AccessRole): DropdownMenuItem[][] {
|
|
|
116
129
|
</div>
|
|
117
130
|
|
|
118
131
|
<!-- Loading -->
|
|
119
|
-
<div
|
|
132
|
+
<div
|
|
133
|
+
v-if="status === 'loading'"
|
|
134
|
+
class="flex flex-col gap-y-3"
|
|
135
|
+
aria-busy="true"
|
|
136
|
+
>
|
|
120
137
|
<USkeleton v-for="row in 3" :key="row" class="h-12 w-full" />
|
|
121
138
|
</div>
|
|
122
139
|
|
|
@@ -125,18 +142,32 @@ function menuItems(role: AccessRole): DropdownMenuItem[][] {
|
|
|
125
142
|
v-else-if="status === 'error'"
|
|
126
143
|
:title="t('supabase.access.roles.error.title')"
|
|
127
144
|
:description="loadError"
|
|
128
|
-
:actions="[
|
|
145
|
+
:actions="[
|
|
146
|
+
{
|
|
147
|
+
label: t('pluto.common.tryAgain'),
|
|
148
|
+
color: 'neutral',
|
|
149
|
+
variant: 'outline',
|
|
150
|
+
onClick: load,
|
|
151
|
+
},
|
|
152
|
+
]"
|
|
129
153
|
color="error"
|
|
130
154
|
variant="subtle"
|
|
131
155
|
icon="lucide:circle-x"
|
|
132
156
|
/>
|
|
133
157
|
|
|
134
158
|
<!-- The list -->
|
|
135
|
-
<UTable
|
|
159
|
+
<UTable
|
|
160
|
+
v-else
|
|
161
|
+
:data="roles"
|
|
162
|
+
:columns="columns"
|
|
163
|
+
class="border-default rounded-lg border"
|
|
164
|
+
>
|
|
136
165
|
<template #role-cell="{ row }">
|
|
137
166
|
<div class="flex min-w-0 flex-col">
|
|
138
167
|
<span class="flex items-center gap-2">
|
|
139
|
-
<span class="text-highlighted truncate font-medium">{{
|
|
168
|
+
<span class="text-highlighted truncate font-medium">{{
|
|
169
|
+
row.original.label
|
|
170
|
+
}}</span>
|
|
140
171
|
<UBadge
|
|
141
172
|
v-if="row.original.is_builtin"
|
|
142
173
|
:label="t('supabase.access.roles.builtin')"
|
|
@@ -145,7 +176,9 @@ function menuItems(role: AccessRole): DropdownMenuItem[][] {
|
|
|
145
176
|
size="sm"
|
|
146
177
|
/>
|
|
147
178
|
</span>
|
|
148
|
-
<code class="text-muted truncate font-mono text-xs">{{
|
|
179
|
+
<code class="text-muted truncate font-mono text-xs">{{
|
|
180
|
+
row.original.key
|
|
181
|
+
}}</code>
|
|
149
182
|
</div>
|
|
150
183
|
</template>
|
|
151
184
|
|
|
@@ -154,17 +187,27 @@ function menuItems(role: AccessRole): DropdownMenuItem[][] {
|
|
|
154
187
|
{{ t('supabase.access.roles.fullAccess') }}
|
|
155
188
|
</span>
|
|
156
189
|
<span v-else class="text-sm">
|
|
157
|
-
{{
|
|
190
|
+
{{
|
|
191
|
+
t(
|
|
192
|
+
'supabase.access.roles.permissionCount',
|
|
193
|
+
row.original.capabilities.length
|
|
194
|
+
)
|
|
195
|
+
}}
|
|
158
196
|
</span>
|
|
159
197
|
</template>
|
|
160
198
|
|
|
161
199
|
<template #users-cell="{ row }">
|
|
162
|
-
<span class="text-sm">{{
|
|
200
|
+
<span class="text-sm">{{
|
|
201
|
+
t('supabase.access.roles.userCount', row.original.user_count)
|
|
202
|
+
}}</span>
|
|
163
203
|
</template>
|
|
164
204
|
|
|
165
205
|
<template #actions-cell="{ row }">
|
|
166
206
|
<div class="flex justify-end">
|
|
167
|
-
<UDropdownMenu
|
|
207
|
+
<UDropdownMenu
|
|
208
|
+
:items="menuItems(row.original)"
|
|
209
|
+
:content="{ align: 'end' }"
|
|
210
|
+
>
|
|
168
211
|
<UButton
|
|
169
212
|
:aria-label="row.original.label"
|
|
170
213
|
icon="lucide:ellipsis"
|
|
@@ -17,10 +17,18 @@ import {
|
|
|
17
17
|
} from '../../../utils/access-ui'
|
|
18
18
|
|
|
19
19
|
// The modals and the slideover load on demand. They stay out of the page chunk.
|
|
20
|
-
const InviteModal = defineAsyncComponent(
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
const
|
|
20
|
+
const InviteModal = defineAsyncComponent(
|
|
21
|
+
() => import('../../../components/access/InviteModal.vue')
|
|
22
|
+
)
|
|
23
|
+
const EditRolesSlideover = defineAsyncComponent(
|
|
24
|
+
() => import('../../../components/access/EditRolesSlideover.vue')
|
|
25
|
+
)
|
|
26
|
+
const RevokeModal = defineAsyncComponent(
|
|
27
|
+
() => import('../../../components/access/RevokeModal.vue')
|
|
28
|
+
)
|
|
29
|
+
const LinkModal = defineAsyncComponent(
|
|
30
|
+
() => import('../../../components/access/LinkModal.vue')
|
|
31
|
+
)
|
|
24
32
|
|
|
25
33
|
const { t, d, locale } = usePlutoI18n()
|
|
26
34
|
const toast = useToast()
|
|
@@ -30,7 +38,8 @@ const permissions = usePlutoPermissions()
|
|
|
30
38
|
const auth = usePlutoAuth()
|
|
31
39
|
|
|
32
40
|
useHead({
|
|
33
|
-
title: () =>
|
|
41
|
+
title: () =>
|
|
42
|
+
`${t('supabase.access.users.title')} - ${t('pluto.pages.settings')}`,
|
|
34
43
|
})
|
|
35
44
|
|
|
36
45
|
const users = ref<AccessUser[]>([])
|
|
@@ -65,7 +74,9 @@ async function load() {
|
|
|
65
74
|
|
|
66
75
|
async function loadCapabilities() {
|
|
67
76
|
try {
|
|
68
|
-
capabilities.value = await $fetch<AccessCapabilities>(
|
|
77
|
+
capabilities.value = await $fetch<AccessCapabilities>(
|
|
78
|
+
'/api/_pluto/supabase/access/capabilities'
|
|
79
|
+
)
|
|
69
80
|
} catch {
|
|
70
81
|
// The invite works without it. Only the "Copy invite link" action hides.
|
|
71
82
|
capabilities.value = null
|
|
@@ -75,11 +86,15 @@ async function loadCapabilities() {
|
|
|
75
86
|
onMounted(load)
|
|
76
87
|
|
|
77
88
|
// Only an admin can read this, so ask after the permissions are known.
|
|
78
|
-
watch(
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
89
|
+
watch(
|
|
90
|
+
isAdmin,
|
|
91
|
+
(admin) => {
|
|
92
|
+
if (admin && !capabilities.value) {
|
|
93
|
+
void loadCapabilities()
|
|
94
|
+
}
|
|
95
|
+
},
|
|
96
|
+
{ immediate: true }
|
|
97
|
+
)
|
|
83
98
|
|
|
84
99
|
const columns = computed<TableColumn<AccessUser>[]>(() => [
|
|
85
100
|
{ id: 'user', header: t('supabase.access.users.columns.user') },
|
|
@@ -89,7 +104,10 @@ const columns = computed<TableColumn<AccessUser>[]>(() => [
|
|
|
89
104
|
{ id: 'actions', header: '' },
|
|
90
105
|
])
|
|
91
106
|
|
|
92
|
-
function dateText(
|
|
107
|
+
function dateText(
|
|
108
|
+
value: string | null,
|
|
109
|
+
options: Intl.DateTimeFormatOptions
|
|
110
|
+
): string {
|
|
93
111
|
return value ? d(new Date(value), options) : ''
|
|
94
112
|
}
|
|
95
113
|
|
|
@@ -134,10 +152,13 @@ async function copyLink(user: AccessUser) {
|
|
|
134
152
|
}
|
|
135
153
|
|
|
136
154
|
try {
|
|
137
|
-
const result = await $fetch<{ link: string | null }>(
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
155
|
+
const result = await $fetch<{ link: string | null }>(
|
|
156
|
+
'/api/_pluto/supabase/access/users/invite-link',
|
|
157
|
+
{
|
|
158
|
+
method: 'POST',
|
|
159
|
+
body: { email: user.email, roleKeys: user.roles },
|
|
160
|
+
}
|
|
161
|
+
)
|
|
141
162
|
|
|
142
163
|
// A confirmed account gets no link: it signs in with its own password.
|
|
143
164
|
if (!result.link) {
|
|
@@ -163,7 +184,7 @@ async function copyLink(user: AccessUser) {
|
|
|
163
184
|
// --- Resend ---
|
|
164
185
|
async function resend(user: AccessUser) {
|
|
165
186
|
try {
|
|
166
|
-
const result = await $fetch<{ emailSent?: boolean
|
|
187
|
+
const result = await $fetch<{ emailSent?: boolean; link?: string }>(
|
|
167
188
|
`/api/_pluto/supabase/access/users/${user.id}/resend`,
|
|
168
189
|
{ method: 'POST', body: { locale: locale.value } }
|
|
169
190
|
)
|
|
@@ -238,8 +259,12 @@ function menuItems(user: AccessUser): DropdownMenuItem[][] {
|
|
|
238
259
|
<div class="flex flex-col gap-y-6">
|
|
239
260
|
<div class="flex flex-wrap items-start justify-between gap-4">
|
|
240
261
|
<div>
|
|
241
|
-
<h2 class="text-xl font-semibold">
|
|
242
|
-
|
|
262
|
+
<h2 class="text-xl font-semibold">
|
|
263
|
+
{{ t('supabase.access.users.title') }}
|
|
264
|
+
</h2>
|
|
265
|
+
<p class="text-muted text-sm">
|
|
266
|
+
{{ t('supabase.access.users.description') }}
|
|
267
|
+
</p>
|
|
243
268
|
</div>
|
|
244
269
|
|
|
245
270
|
<UButton v-if="isAdmin" icon="lucide:user-plus" @click="openInvite">
|
|
@@ -248,7 +273,11 @@ function menuItems(user: AccessUser): DropdownMenuItem[][] {
|
|
|
248
273
|
</div>
|
|
249
274
|
|
|
250
275
|
<!-- Loading -->
|
|
251
|
-
<div
|
|
276
|
+
<div
|
|
277
|
+
v-if="status === 'loading'"
|
|
278
|
+
class="flex flex-col gap-y-3"
|
|
279
|
+
aria-busy="true"
|
|
280
|
+
>
|
|
252
281
|
<USkeleton v-for="row in 4" :key="row" class="h-12 w-full" />
|
|
253
282
|
</div>
|
|
254
283
|
|
|
@@ -257,7 +286,14 @@ function menuItems(user: AccessUser): DropdownMenuItem[][] {
|
|
|
257
286
|
v-else-if="status === 'error'"
|
|
258
287
|
:title="t('supabase.access.users.error.title')"
|
|
259
288
|
:description="loadError"
|
|
260
|
-
:actions="[
|
|
289
|
+
:actions="[
|
|
290
|
+
{
|
|
291
|
+
label: t('pluto.common.tryAgain'),
|
|
292
|
+
color: 'neutral',
|
|
293
|
+
variant: 'outline',
|
|
294
|
+
onClick: load,
|
|
295
|
+
},
|
|
296
|
+
]"
|
|
261
297
|
color="error"
|
|
262
298
|
variant="subtle"
|
|
263
299
|
icon="lucide:circle-x"
|
|
@@ -268,16 +304,33 @@ function menuItems(user: AccessUser): DropdownMenuItem[][] {
|
|
|
268
304
|
v-else-if="users.length === 0"
|
|
269
305
|
:title="t('supabase.access.users.empty.title')"
|
|
270
306
|
:description="t('supabase.access.users.empty.description')"
|
|
271
|
-
:actions="
|
|
307
|
+
:actions="
|
|
308
|
+
isAdmin
|
|
309
|
+
? [
|
|
310
|
+
{
|
|
311
|
+
label: t('supabase.access.users.invite'),
|
|
312
|
+
icon: 'lucide:user-plus',
|
|
313
|
+
onClick: openInvite,
|
|
314
|
+
},
|
|
315
|
+
]
|
|
316
|
+
: []
|
|
317
|
+
"
|
|
272
318
|
icon="lucide:users"
|
|
273
319
|
/>
|
|
274
320
|
|
|
275
321
|
<!-- The list -->
|
|
276
|
-
<UTable
|
|
322
|
+
<UTable
|
|
323
|
+
v-else
|
|
324
|
+
:data="users"
|
|
325
|
+
:columns="columns"
|
|
326
|
+
class="border-default rounded-lg border"
|
|
327
|
+
>
|
|
277
328
|
<template #user-cell="{ row }">
|
|
278
329
|
<div class="flex min-w-0 flex-col">
|
|
279
330
|
<span class="flex items-center gap-2">
|
|
280
|
-
<span class="text-highlighted truncate font-medium">{{
|
|
331
|
+
<span class="text-highlighted truncate font-medium">{{
|
|
332
|
+
userDisplayName(row.original)
|
|
333
|
+
}}</span>
|
|
281
334
|
<UBadge
|
|
282
335
|
v-if="row.original.id === selfId"
|
|
283
336
|
:label="t('supabase.access.users.you')"
|
|
@@ -286,7 +339,9 @@ function menuItems(user: AccessUser): DropdownMenuItem[][] {
|
|
|
286
339
|
size="sm"
|
|
287
340
|
/>
|
|
288
341
|
</span>
|
|
289
|
-
<span v-if="row.original.email" class="text-muted truncate text-sm">{{
|
|
342
|
+
<span v-if="row.original.email" class="text-muted truncate text-sm">{{
|
|
343
|
+
row.original.email
|
|
344
|
+
}}</span>
|
|
290
345
|
</div>
|
|
291
346
|
</template>
|
|
292
347
|
|
|
@@ -325,7 +380,13 @@ function menuItems(user: AccessUser): DropdownMenuItem[][] {
|
|
|
325
380
|
variant="subtle"
|
|
326
381
|
/>
|
|
327
382
|
<span v-if="row.original.invited_at" class="text-muted text-xs">
|
|
328
|
-
{{
|
|
383
|
+
{{
|
|
384
|
+
t('supabase.access.users.status.invitedOn', {
|
|
385
|
+
date: dateText(row.original.invited_at, {
|
|
386
|
+
dateStyle: 'medium',
|
|
387
|
+
}),
|
|
388
|
+
})
|
|
389
|
+
}}
|
|
329
390
|
</span>
|
|
330
391
|
</template>
|
|
331
392
|
</div>
|
|
@@ -333,16 +394,30 @@ function menuItems(user: AccessUser): DropdownMenuItem[][] {
|
|
|
333
394
|
|
|
334
395
|
<template #lastSignIn-cell="{ row }">
|
|
335
396
|
<span v-if="row.original.last_sign_in_at" class="text-sm">
|
|
336
|
-
{{
|
|
397
|
+
{{
|
|
398
|
+
dateText(row.original.last_sign_in_at, {
|
|
399
|
+
dateStyle: 'medium',
|
|
400
|
+
timeStyle: 'short',
|
|
401
|
+
})
|
|
402
|
+
}}
|
|
337
403
|
</span>
|
|
338
|
-
<span v-else class="text-muted text-sm">{{
|
|
404
|
+
<span v-else class="text-muted text-sm">{{
|
|
405
|
+
t('supabase.access.users.neverSignedIn')
|
|
406
|
+
}}</span>
|
|
339
407
|
</template>
|
|
340
408
|
|
|
341
409
|
<template #actions-cell="{ row }">
|
|
342
410
|
<div v-if="isAdmin" class="flex justify-end">
|
|
343
|
-
<UDropdownMenu
|
|
411
|
+
<UDropdownMenu
|
|
412
|
+
:items="menuItems(row.original)"
|
|
413
|
+
:content="{ align: 'end' }"
|
|
414
|
+
>
|
|
344
415
|
<UButton
|
|
345
|
-
:aria-label="
|
|
416
|
+
:aria-label="
|
|
417
|
+
t('supabase.access.users.actionsLabel', {
|
|
418
|
+
name: userDisplayName(row.original),
|
|
419
|
+
})
|
|
420
|
+
"
|
|
346
421
|
icon="lucide:ellipsis"
|
|
347
422
|
color="neutral"
|
|
348
423
|
variant="ghost"
|
|
@@ -225,7 +225,10 @@ function handleStepChange(step: number) {
|
|
|
225
225
|
class="flex flex-col gap-y-4"
|
|
226
226
|
@submit="handleStepChange(currentStep)"
|
|
227
227
|
>
|
|
228
|
-
<UFormField
|
|
228
|
+
<UFormField
|
|
229
|
+
:label="t('supabase.connection.connectionString')"
|
|
230
|
+
required
|
|
231
|
+
>
|
|
229
232
|
<UInput
|
|
230
233
|
v-model="databaseForm.connectionString"
|
|
231
234
|
:disabled="isSettingUpDatabase"
|
|
@@ -245,7 +248,9 @@ function handleStepChange(step: number) {
|
|
|
245
248
|
class="underline"
|
|
246
249
|
external
|
|
247
250
|
>
|
|
248
|
-
{{
|
|
251
|
+
{{
|
|
252
|
+
t('supabase.connection.connectionStringHelpLink')
|
|
253
|
+
}}
|
|
249
254
|
</ULink>
|
|
250
255
|
</template>
|
|
251
256
|
</SupabaseMessageParts>
|
|
@@ -296,7 +301,9 @@ function handleStepChange(step: number) {
|
|
|
296
301
|
:message="t('supabase.setup.why')"
|
|
297
302
|
>
|
|
298
303
|
<template #promise>
|
|
299
|
-
<UTooltip
|
|
304
|
+
<UTooltip
|
|
305
|
+
:text="t('supabase.setup.promiseTooltip')"
|
|
306
|
+
>
|
|
300
307
|
<span
|
|
301
308
|
class="underline decoration-dotted cursor-help"
|
|
302
309
|
>
|
|
@@ -324,7 +331,9 @@ function handleStepChange(step: number) {
|
|
|
324
331
|
>
|
|
325
332
|
<p>
|
|
326
333
|
<SupabaseMessageParts
|
|
327
|
-
:message="
|
|
334
|
+
:message="
|
|
335
|
+
t('supabase.setup.technicalDetailsRequest')
|
|
336
|
+
"
|
|
328
337
|
>
|
|
329
338
|
<template #route>
|
|
330
339
|
<ULink
|
|
@@ -332,7 +341,11 @@ function handleStepChange(step: number) {
|
|
|
332
341
|
target="_blank"
|
|
333
342
|
class="underline"
|
|
334
343
|
>
|
|
335
|
-
{{
|
|
344
|
+
{{
|
|
345
|
+
t(
|
|
346
|
+
'supabase.setup.technicalDetailsRoute'
|
|
347
|
+
)
|
|
348
|
+
}}</ULink
|
|
336
349
|
>
|
|
337
350
|
</template>
|
|
338
351
|
<template #files>
|
|
@@ -341,7 +354,11 @@ function handleStepChange(step: number) {
|
|
|
341
354
|
target="_blank"
|
|
342
355
|
class="underline"
|
|
343
356
|
>
|
|
344
|
-
{{
|
|
357
|
+
{{
|
|
358
|
+
t(
|
|
359
|
+
'supabase.setup.technicalDetailsFiles'
|
|
360
|
+
)
|
|
361
|
+
}}</ULink
|
|
345
362
|
>
|
|
346
363
|
</template>
|
|
347
364
|
</SupabaseMessageParts>
|
|
@@ -374,7 +391,9 @@ function handleStepChange(step: number) {
|
|
|
374
391
|
<hgroup class="text-center">
|
|
375
392
|
<span class="text-3xl">🎉</span>
|
|
376
393
|
|
|
377
|
-
<h1 class="font-bold text-2xl">
|
|
394
|
+
<h1 class="font-bold text-2xl">
|
|
395
|
+
{{ t('supabase.setup.doneTitle') }}
|
|
396
|
+
</h1>
|
|
378
397
|
</hgroup>
|
|
379
398
|
|
|
380
399
|
<p class="text-balance text-center text-lg">
|