@plutocms/core 0.47.1 → 0.48.0
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/CHANGELOG.md +11 -0
- package/FEATURES.md +15 -4
- package/app/components/Pluto/Admin/PlutoAdminBackendBadge.vue +84 -0
- package/app/components/Pluto/Admin/PlutoAdminNavbar.vue +7 -0
- package/app/components/Pluto/Github/PlutoGithubBackendDetails.vue +180 -0
- package/app/components/Pluto/Github/PlutoGithubBuildStatus.vue +17 -4
- package/app/components/Pluto/Github/PlutoGithubDeployPage.vue +472 -0
- package/app/components/Pluto/Github/PlutoGithubUsersPage.vue +214 -0
- package/app/components/Pluto/PlutoHtml.vue +8 -2
- package/app/composables/internal/async-component.ts +9 -0
- package/app/composables/pluto-extension.ts +4 -0
- package/app/composables/pluto-navbar.ts +25 -0
- package/app/composables/pluto-registry.ts +1 -0
- package/i18n/locales/en.json +164 -2
- package/i18n/locales/pt-BR.json +164 -2
- package/modules/pluto-github/index.ts +127 -51
- package/modules/pluto-github/options.ts +18 -5
- package/modules/pluto-github/runtime/app/composables/pluto-github-local-git.ts +50 -0
- package/modules/pluto-github/runtime/app/composables/pluto-github-session.ts +8 -1
- package/modules/pluto-github/runtime/app/plugins/pluto-github.ts +84 -1
- package/modules/pluto-github/runtime/server/adapter.ts +26 -7
- package/modules/pluto-github/runtime/server/api/deploy/pages.post.ts +5 -0
- package/modules/pluto-github/runtime/server/api/deploy/status.get.ts +5 -0
- package/modules/pluto-github/runtime/server/api/local/push.post.ts +5 -0
- package/modules/pluto-github/runtime/server/api/local/status.get.ts +5 -0
- package/modules/pluto-github/runtime/server/api/users.get.ts +5 -0
- package/modules/pluto-github/runtime/server/store/github.ts +4 -1
- package/modules/pluto-github/runtime/server/store/local-git.ts +145 -0
- package/modules/pluto-github/runtime/server/store/memory.ts +4 -1
- package/modules/pluto-github/runtime/server/utils/build-status.ts +16 -7
- package/modules/pluto-github/runtime/server/utils/deploy.ts +204 -0
- package/modules/pluto-github/runtime/server/utils/github-setup.ts +1 -3
- package/modules/pluto-github/runtime/server/utils/github-users.ts +120 -0
- package/modules/pluto-github/runtime/server/utils/local-git-routes.ts +95 -0
- package/modules/pluto-github/runtime/shared/build-status.ts +26 -0
- package/modules/pluto-github/runtime/shared/client-rules.ts +16 -0
- package/modules/pluto-github/runtime/shared/deploy.ts +43 -0
- package/modules/pluto-github/runtime/shared/pages-workflow.ts +138 -0
- package/modules/pluto-github/runtime/shared/repo-users.ts +97 -0
- package/package.json +1 -1
- package/shared/types/github.d.ts +2 -0
- package/shared/types/registry.ts +22 -0
- package/shared/utils/html-base.ts +23 -0
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The Deploy tab of Settings (`/admin/settings/deploy`). The CMS module adds
|
|
3
|
+
// the route with `extendPages`, and the client plugin adds the tab. It puts
|
|
4
|
+
// the site on a host:
|
|
5
|
+
//
|
|
6
|
+
// - The whole app (the site and the admin): Vercel, Netlify, a Node host. A
|
|
7
|
+
// card opens the import page of the host for this repo.
|
|
8
|
+
// - A static site (the admin stays on the computer): GitHub Pages and
|
|
9
|
+
// Cloudflare Pages. In local mode, "Set up GitHub Pages" writes the
|
|
10
|
+
// workflow, commits it, and turns Pages on with the GitHub CLI.
|
|
11
|
+
//
|
|
12
|
+
// The file lives under `app/` so Tailwind scans it. See the git-cms skill.
|
|
13
|
+
import type {
|
|
14
|
+
PlutoDeployStatus,
|
|
15
|
+
PlutoPagesSetupResult,
|
|
16
|
+
} from '../../../../modules/pluto-github/runtime/shared/deploy'
|
|
17
|
+
import { usePlutoGithubLocalGit } from '../../../../modules/pluto-github/runtime/app/composables/pluto-github-local-git'
|
|
18
|
+
|
|
19
|
+
const DOCS_URL = 'https://pluto.vulmix.dev/docs/start-here/deploy'
|
|
20
|
+
|
|
21
|
+
const { t } = usePlutoI18n()
|
|
22
|
+
const errorText = usePlutoErrorText()
|
|
23
|
+
const localGit = usePlutoGithubLocalGit()
|
|
24
|
+
|
|
25
|
+
useHead({
|
|
26
|
+
title: () =>
|
|
27
|
+
`${t('pluto.settings.tabs.deploy')} - ${t('pluto.pages.settings')}`,
|
|
28
|
+
})
|
|
29
|
+
|
|
30
|
+
const status = ref<PlutoDeployStatus | null>(null)
|
|
31
|
+
const loadError = ref('')
|
|
32
|
+
const loading = ref(true)
|
|
33
|
+
|
|
34
|
+
async function load() {
|
|
35
|
+
loading.value = true
|
|
36
|
+
loadError.value = ''
|
|
37
|
+
try {
|
|
38
|
+
status.value = await $fetch<PlutoDeployStatus>(
|
|
39
|
+
'/api/_pluto/github/deploy/status'
|
|
40
|
+
)
|
|
41
|
+
} catch (error) {
|
|
42
|
+
loadError.value = errorText(error)
|
|
43
|
+
} finally {
|
|
44
|
+
loading.value = false
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
onMounted(load)
|
|
48
|
+
|
|
49
|
+
const repoUrl = computed(() =>
|
|
50
|
+
status.value?.repo ? `https://github.com/${status.value.repo}` : ''
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
/** The full-site hosts. Each link opens the import page of the host. */
|
|
54
|
+
const hosts = computed(() => [
|
|
55
|
+
{
|
|
56
|
+
id: 'vercel',
|
|
57
|
+
icon: 'i-lucide-triangle',
|
|
58
|
+
to: repoUrl.value
|
|
59
|
+
? `https://vercel.com/new/import?s=${encodeURIComponent(repoUrl.value)}`
|
|
60
|
+
: 'https://vercel.com/new',
|
|
61
|
+
},
|
|
62
|
+
{
|
|
63
|
+
id: 'netlify',
|
|
64
|
+
icon: 'i-lucide-diamond',
|
|
65
|
+
to: 'https://app.netlify.com/start',
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
id: 'node',
|
|
69
|
+
icon: 'i-lucide-server',
|
|
70
|
+
to: `${DOCS_URL}#a-node-host`,
|
|
71
|
+
},
|
|
72
|
+
])
|
|
73
|
+
|
|
74
|
+
// --- GitHub Pages ------------------------------------------------------------
|
|
75
|
+
|
|
76
|
+
const settingUp = ref(false)
|
|
77
|
+
const setupError = ref('')
|
|
78
|
+
const setupResult = ref<PlutoPagesSetupResult | null>(null)
|
|
79
|
+
|
|
80
|
+
const pages = computed(() => status.value?.pages)
|
|
81
|
+
const pagesReady = computed(
|
|
82
|
+
() =>
|
|
83
|
+
Boolean(status.value?.workflow.current) &&
|
|
84
|
+
pages.value?.state === 'on' &&
|
|
85
|
+
pages.value.source === 'workflow'
|
|
86
|
+
)
|
|
87
|
+
const pagesUrl = computed(
|
|
88
|
+
() => pages.value?.url || status.value?.pagesUrl || ''
|
|
89
|
+
)
|
|
90
|
+
const pagesSettingsUrl = computed(() =>
|
|
91
|
+
repoUrl.value ? `${repoUrl.value}/settings/pages` : ''
|
|
92
|
+
)
|
|
93
|
+
|
|
94
|
+
/** One line for the state of Pages on GitHub. */
|
|
95
|
+
const pagesLine = computed(() => {
|
|
96
|
+
const current = pages.value
|
|
97
|
+
if (!current) {
|
|
98
|
+
return ''
|
|
99
|
+
}
|
|
100
|
+
if (current.state === 'on') {
|
|
101
|
+
return current.source === 'workflow'
|
|
102
|
+
? t('pluto.github.deploy.pages.state.on')
|
|
103
|
+
: t('pluto.github.deploy.pages.state.legacy')
|
|
104
|
+
}
|
|
105
|
+
if (current.state === 'off') {
|
|
106
|
+
return t('pluto.github.deploy.pages.state.off')
|
|
107
|
+
}
|
|
108
|
+
return t(`pluto.github.deploy.pages.reason.${current.reason ?? 'error'}`)
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
async function setupPages() {
|
|
112
|
+
settingUp.value = true
|
|
113
|
+
setupError.value = ''
|
|
114
|
+
try {
|
|
115
|
+
const result = await $fetch<PlutoPagesSetupResult>(
|
|
116
|
+
'/api/_pluto/github/deploy/pages',
|
|
117
|
+
{ method: 'POST' }
|
|
118
|
+
)
|
|
119
|
+
setupResult.value = result
|
|
120
|
+
status.value = result.status
|
|
121
|
+
// The workflow commit is a new commit to push: the Git badge counts it.
|
|
122
|
+
localGit.refresh()
|
|
123
|
+
} catch (error) {
|
|
124
|
+
setupError.value = errorText(error)
|
|
125
|
+
} finally {
|
|
126
|
+
settingUp.value = false
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const showWorkflow = ref(false)
|
|
131
|
+
const copied = ref('')
|
|
132
|
+
async function copy(id: string, text: string) {
|
|
133
|
+
try {
|
|
134
|
+
await navigator.clipboard.writeText(text)
|
|
135
|
+
copied.value = id
|
|
136
|
+
setTimeout(() => {
|
|
137
|
+
if (copied.value === id) {
|
|
138
|
+
copied.value = ''
|
|
139
|
+
}
|
|
140
|
+
}, 2000)
|
|
141
|
+
} catch {
|
|
142
|
+
// The text is selectable, so the user can copy it by hand.
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
const cloudflare = {
|
|
147
|
+
command: 'npx nuxt generate',
|
|
148
|
+
output: '.output/public',
|
|
149
|
+
url: 'https://dash.cloudflare.com/?to=/:account/workers-and-pages/create/pages',
|
|
150
|
+
}
|
|
151
|
+
</script>
|
|
152
|
+
|
|
153
|
+
<template>
|
|
154
|
+
<div class="flex flex-col gap-y-8">
|
|
155
|
+
<div>
|
|
156
|
+
<h2 class="text-xl font-semibold">
|
|
157
|
+
{{ t('pluto.settings.tabs.deploy') }}
|
|
158
|
+
</h2>
|
|
159
|
+
<p class="text-muted text-sm">
|
|
160
|
+
{{ t('pluto.github.deploy.intro') }}
|
|
161
|
+
</p>
|
|
162
|
+
</div>
|
|
163
|
+
|
|
164
|
+
<UAlert
|
|
165
|
+
v-if="loadError"
|
|
166
|
+
:title="t('pluto.github.deploy.error')"
|
|
167
|
+
:description="loadError"
|
|
168
|
+
:actions="[
|
|
169
|
+
{
|
|
170
|
+
label: t('pluto.common.tryAgain'),
|
|
171
|
+
color: 'neutral',
|
|
172
|
+
variant: 'outline',
|
|
173
|
+
onClick: load,
|
|
174
|
+
},
|
|
175
|
+
]"
|
|
176
|
+
color="error"
|
|
177
|
+
icon="i-lucide-circle-x"
|
|
178
|
+
variant="subtle"
|
|
179
|
+
/>
|
|
180
|
+
|
|
181
|
+
<!-- The whole app: the site and the admin. -->
|
|
182
|
+
<section class="flex flex-col gap-y-3">
|
|
183
|
+
<div>
|
|
184
|
+
<h3 class="font-semibold">{{ t('pluto.github.deploy.full.title') }}</h3>
|
|
185
|
+
<p class="text-muted text-sm">
|
|
186
|
+
{{ t('pluto.github.deploy.full.description') }}
|
|
187
|
+
</p>
|
|
188
|
+
</div>
|
|
189
|
+
<div class="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
|
|
190
|
+
<UCard v-for="host in hosts" :key="host.id">
|
|
191
|
+
<div class="flex h-full flex-col gap-y-3">
|
|
192
|
+
<div class="flex items-center gap-2">
|
|
193
|
+
<UIcon :name="host.icon" class="size-5" />
|
|
194
|
+
<span class="font-semibold">
|
|
195
|
+
{{ t(`pluto.github.deploy.hosts.${host.id}.name`) }}
|
|
196
|
+
</span>
|
|
197
|
+
</div>
|
|
198
|
+
<p class="text-muted grow text-sm">
|
|
199
|
+
{{ t(`pluto.github.deploy.hosts.${host.id}.description`) }}
|
|
200
|
+
</p>
|
|
201
|
+
<UButton
|
|
202
|
+
:label="t(`pluto.github.deploy.hosts.${host.id}.action`)"
|
|
203
|
+
:to="host.to"
|
|
204
|
+
:color="host.id === 'node' ? 'neutral' : 'primary'"
|
|
205
|
+
:variant="host.id === 'node' ? 'outline' : 'solid'"
|
|
206
|
+
class="self-start"
|
|
207
|
+
target="_blank"
|
|
208
|
+
trailing-icon="i-lucide-external-link"
|
|
209
|
+
/>
|
|
210
|
+
</div>
|
|
211
|
+
</UCard>
|
|
212
|
+
</div>
|
|
213
|
+
<p class="text-muted text-sm">
|
|
214
|
+
{{ t('pluto.github.deploy.full.after') }}
|
|
215
|
+
</p>
|
|
216
|
+
</section>
|
|
217
|
+
|
|
218
|
+
<!-- A static site: the admin stays on the computer. -->
|
|
219
|
+
<section class="flex flex-col gap-y-3">
|
|
220
|
+
<div>
|
|
221
|
+
<h3 class="font-semibold">
|
|
222
|
+
{{ t('pluto.github.deploy.static.title') }}
|
|
223
|
+
</h3>
|
|
224
|
+
<p class="text-muted text-sm">
|
|
225
|
+
{{ t('pluto.github.deploy.static.description') }}
|
|
226
|
+
</p>
|
|
227
|
+
</div>
|
|
228
|
+
|
|
229
|
+
<UCard>
|
|
230
|
+
<div class="flex flex-col gap-y-4">
|
|
231
|
+
<div class="flex flex-wrap items-center justify-between gap-3">
|
|
232
|
+
<div class="flex items-center gap-2">
|
|
233
|
+
<UIcon name="i-pluto-github" class="size-5" />
|
|
234
|
+
<span class="font-semibold">
|
|
235
|
+
{{ t('pluto.github.deploy.pages.name') }}
|
|
236
|
+
</span>
|
|
237
|
+
<UBadge
|
|
238
|
+
v-if="pagesReady"
|
|
239
|
+
:label="t('pluto.github.deploy.pages.ready')"
|
|
240
|
+
color="success"
|
|
241
|
+
variant="subtle"
|
|
242
|
+
/>
|
|
243
|
+
</div>
|
|
244
|
+
<UButton
|
|
245
|
+
v-if="pagesReady && pagesUrl"
|
|
246
|
+
:label="t('pluto.github.deploy.pages.open')"
|
|
247
|
+
:to="pagesUrl"
|
|
248
|
+
color="neutral"
|
|
249
|
+
target="_blank"
|
|
250
|
+
trailing-icon="i-lucide-external-link"
|
|
251
|
+
variant="outline"
|
|
252
|
+
/>
|
|
253
|
+
</div>
|
|
254
|
+
|
|
255
|
+
<p class="text-muted text-sm">
|
|
256
|
+
{{ t('pluto.github.deploy.pages.description') }}
|
|
257
|
+
</p>
|
|
258
|
+
|
|
259
|
+
<USkeleton v-if="loading && !status" class="h-16 w-full" />
|
|
260
|
+
|
|
261
|
+
<!-- Local mode: the checks and the Set up button. -->
|
|
262
|
+
<template v-else-if="status?.local">
|
|
263
|
+
<ul class="flex flex-col gap-y-2 text-sm">
|
|
264
|
+
<li class="flex items-start gap-2">
|
|
265
|
+
<UIcon
|
|
266
|
+
:name="
|
|
267
|
+
status.workflow.current
|
|
268
|
+
? 'i-lucide-circle-check'
|
|
269
|
+
: 'i-lucide-circle-dashed'
|
|
270
|
+
"
|
|
271
|
+
:class="
|
|
272
|
+
status.workflow.current ? 'text-success' : 'text-muted'
|
|
273
|
+
"
|
|
274
|
+
class="mt-0.5 size-4 shrink-0"
|
|
275
|
+
/>
|
|
276
|
+
<span>
|
|
277
|
+
{{
|
|
278
|
+
status.workflow.current
|
|
279
|
+
? t('pluto.github.deploy.pages.workflow.done')
|
|
280
|
+
: status.workflow.exists
|
|
281
|
+
? t('pluto.github.deploy.pages.workflow.old')
|
|
282
|
+
: t('pluto.github.deploy.pages.workflow.missing')
|
|
283
|
+
}}
|
|
284
|
+
<code class="text-xs">{{ status.workflow.path }}</code>
|
|
285
|
+
</span>
|
|
286
|
+
</li>
|
|
287
|
+
<li class="flex items-start gap-2">
|
|
288
|
+
<UIcon
|
|
289
|
+
:name="
|
|
290
|
+
pages?.state === 'on' && pages.source === 'workflow'
|
|
291
|
+
? 'i-lucide-circle-check'
|
|
292
|
+
: 'i-lucide-circle-dashed'
|
|
293
|
+
"
|
|
294
|
+
:class="
|
|
295
|
+
pages?.state === 'on' && pages.source === 'workflow'
|
|
296
|
+
? 'text-success'
|
|
297
|
+
: 'text-muted'
|
|
298
|
+
"
|
|
299
|
+
class="mt-0.5 size-4 shrink-0"
|
|
300
|
+
/>
|
|
301
|
+
<span>{{ pagesLine }}</span>
|
|
302
|
+
</li>
|
|
303
|
+
</ul>
|
|
304
|
+
|
|
305
|
+
<div class="flex flex-wrap items-center gap-2">
|
|
306
|
+
<UButton
|
|
307
|
+
v-if="!pagesReady"
|
|
308
|
+
:label="t('pluto.github.deploy.pages.setup')"
|
|
309
|
+
:loading="settingUp"
|
|
310
|
+
:disabled="!status.repo"
|
|
311
|
+
icon="i-lucide-wand-sparkles"
|
|
312
|
+
@click="setupPages"
|
|
313
|
+
/>
|
|
314
|
+
<UButton
|
|
315
|
+
v-if="pages?.state !== 'on' || pages.source !== 'workflow'"
|
|
316
|
+
:label="t('pluto.github.deploy.pages.settings')"
|
|
317
|
+
:to="pagesSettingsUrl"
|
|
318
|
+
:disabled="!pagesSettingsUrl"
|
|
319
|
+
color="neutral"
|
|
320
|
+
target="_blank"
|
|
321
|
+
trailing-icon="i-lucide-external-link"
|
|
322
|
+
variant="outline"
|
|
323
|
+
/>
|
|
324
|
+
<UButton
|
|
325
|
+
:label="
|
|
326
|
+
showWorkflow
|
|
327
|
+
? t('pluto.github.deploy.pages.hideWorkflow')
|
|
328
|
+
: t('pluto.github.deploy.pages.showWorkflow')
|
|
329
|
+
"
|
|
330
|
+
color="neutral"
|
|
331
|
+
variant="ghost"
|
|
332
|
+
@click="showWorkflow = !showWorkflow"
|
|
333
|
+
/>
|
|
334
|
+
</div>
|
|
335
|
+
|
|
336
|
+
<UAlert
|
|
337
|
+
v-if="setupError"
|
|
338
|
+
:description="setupError"
|
|
339
|
+
color="error"
|
|
340
|
+
icon="i-lucide-circle-x"
|
|
341
|
+
variant="subtle"
|
|
342
|
+
/>
|
|
343
|
+
<UAlert
|
|
344
|
+
v-else-if="setupResult"
|
|
345
|
+
:title="
|
|
346
|
+
setupResult.pagesOn
|
|
347
|
+
? t('pluto.github.deploy.pages.done.title')
|
|
348
|
+
: t('pluto.github.deploy.pages.partial.title')
|
|
349
|
+
"
|
|
350
|
+
:description="
|
|
351
|
+
setupResult.pagesOn
|
|
352
|
+
? t('pluto.github.deploy.pages.done.description')
|
|
353
|
+
: t('pluto.github.deploy.pages.partial.description')
|
|
354
|
+
"
|
|
355
|
+
:color="setupResult.pagesOn ? 'success' : 'warning'"
|
|
356
|
+
:icon="
|
|
357
|
+
setupResult.pagesOn
|
|
358
|
+
? 'i-lucide-circle-check'
|
|
359
|
+
: 'i-lucide-triangle-alert'
|
|
360
|
+
"
|
|
361
|
+
variant="subtle"
|
|
362
|
+
/>
|
|
363
|
+
</template>
|
|
364
|
+
|
|
365
|
+
<!-- Not local: the setup by hand. -->
|
|
366
|
+
<template v-else-if="status">
|
|
367
|
+
<ol class="list-decimal space-y-1 pl-5 text-sm">
|
|
368
|
+
<li>{{ t('pluto.github.deploy.pages.manual.step1') }}</li>
|
|
369
|
+
<li>
|
|
370
|
+
{{ t('pluto.github.deploy.pages.manual.step2') }}
|
|
371
|
+
<ULink
|
|
372
|
+
v-if="pagesSettingsUrl"
|
|
373
|
+
:to="pagesSettingsUrl"
|
|
374
|
+
class="text-primary"
|
|
375
|
+
target="_blank"
|
|
376
|
+
>
|
|
377
|
+
{{ t('pluto.github.deploy.pages.settings') }}
|
|
378
|
+
</ULink>
|
|
379
|
+
</li>
|
|
380
|
+
<li>{{ t('pluto.github.deploy.pages.manual.step3') }}</li>
|
|
381
|
+
</ol>
|
|
382
|
+
</template>
|
|
383
|
+
|
|
384
|
+
<div
|
|
385
|
+
v-if="status && (showWorkflow || !status.local)"
|
|
386
|
+
class="flex flex-col gap-y-2"
|
|
387
|
+
>
|
|
388
|
+
<div class="flex items-center justify-between gap-2">
|
|
389
|
+
<code class="text-muted text-xs">{{ status.workflow.path }}</code>
|
|
390
|
+
<UButton
|
|
391
|
+
:label="
|
|
392
|
+
copied === 'workflow'
|
|
393
|
+
? t('pluto.common.copied')
|
|
394
|
+
: t('pluto.common.copy')
|
|
395
|
+
"
|
|
396
|
+
:icon="
|
|
397
|
+
copied === 'workflow' ? 'i-lucide-check' : 'i-lucide-copy'
|
|
398
|
+
"
|
|
399
|
+
color="neutral"
|
|
400
|
+
size="xs"
|
|
401
|
+
variant="outline"
|
|
402
|
+
@click="copy('workflow', status.workflow.text)"
|
|
403
|
+
/>
|
|
404
|
+
</div>
|
|
405
|
+
<pre
|
|
406
|
+
class="bg-elevated max-h-80 overflow-auto rounded-md p-3 text-xs"
|
|
407
|
+
>{{ status.workflow.text }}</pre>
|
|
408
|
+
</div>
|
|
409
|
+
|
|
410
|
+
<p class="text-muted text-xs">
|
|
411
|
+
{{ t('pluto.github.deploy.pages.nodeAdmin') }}
|
|
412
|
+
</p>
|
|
413
|
+
</div>
|
|
414
|
+
</UCard>
|
|
415
|
+
|
|
416
|
+
<UCard>
|
|
417
|
+
<div class="flex flex-col gap-y-3">
|
|
418
|
+
<div class="flex items-center gap-2">
|
|
419
|
+
<UIcon name="i-lucide-cloud" class="size-5" />
|
|
420
|
+
<span class="font-semibold">
|
|
421
|
+
{{ t('pluto.github.deploy.cloudflare.name') }}
|
|
422
|
+
</span>
|
|
423
|
+
</div>
|
|
424
|
+
<p class="text-muted text-sm">
|
|
425
|
+
{{ t('pluto.github.deploy.cloudflare.description') }}
|
|
426
|
+
</p>
|
|
427
|
+
<dl class="grid grid-cols-[auto_1fr] items-center gap-x-3 gap-y-2">
|
|
428
|
+
<dt class="text-muted text-sm">
|
|
429
|
+
{{ t('pluto.github.deploy.cloudflare.command') }}
|
|
430
|
+
</dt>
|
|
431
|
+
<dd class="flex items-center gap-2">
|
|
432
|
+
<code class="text-sm">{{ cloudflare.command }}</code>
|
|
433
|
+
<UButton
|
|
434
|
+
:aria-label="t('pluto.common.copy')"
|
|
435
|
+
:icon="
|
|
436
|
+
copied === 'command' ? 'i-lucide-check' : 'i-lucide-copy'
|
|
437
|
+
"
|
|
438
|
+
color="neutral"
|
|
439
|
+
size="xs"
|
|
440
|
+
variant="ghost"
|
|
441
|
+
@click="copy('command', cloudflare.command)"
|
|
442
|
+
/>
|
|
443
|
+
</dd>
|
|
444
|
+
<dt class="text-muted text-sm">
|
|
445
|
+
{{ t('pluto.github.deploy.cloudflare.output') }}
|
|
446
|
+
</dt>
|
|
447
|
+
<dd class="flex items-center gap-2">
|
|
448
|
+
<code class="text-sm">{{ cloudflare.output }}</code>
|
|
449
|
+
<UButton
|
|
450
|
+
:aria-label="t('pluto.common.copy')"
|
|
451
|
+
:icon="copied === 'output' ? 'i-lucide-check' : 'i-lucide-copy'"
|
|
452
|
+
color="neutral"
|
|
453
|
+
size="xs"
|
|
454
|
+
variant="ghost"
|
|
455
|
+
@click="copy('output', cloudflare.output)"
|
|
456
|
+
/>
|
|
457
|
+
</dd>
|
|
458
|
+
</dl>
|
|
459
|
+
<UButton
|
|
460
|
+
:label="t('pluto.github.deploy.cloudflare.action')"
|
|
461
|
+
:to="cloudflare.url"
|
|
462
|
+
class="self-start"
|
|
463
|
+
color="neutral"
|
|
464
|
+
target="_blank"
|
|
465
|
+
trailing-icon="i-lucide-external-link"
|
|
466
|
+
variant="outline"
|
|
467
|
+
/>
|
|
468
|
+
</div>
|
|
469
|
+
</UCard>
|
|
470
|
+
</section>
|
|
471
|
+
</div>
|
|
472
|
+
</template>
|
|
@@ -0,0 +1,214 @@
|
|
|
1
|
+
<script setup lang="ts">
|
|
2
|
+
// The Users page of the Git CMS (`/admin/users`). The CMS module adds the
|
|
3
|
+
// route with `extendPages`, and the client plugin adds the Users item of the
|
|
4
|
+
// menu. It lists the people with write access to the content repo, read only:
|
|
5
|
+
// GitHub owns the access, so the page links to the access settings of the
|
|
6
|
+
// repo. See the git-cms skill.
|
|
7
|
+
import type { TableColumn } from '@nuxt/ui'
|
|
8
|
+
import type {
|
|
9
|
+
PlutoGithubRepoUser,
|
|
10
|
+
PlutoGithubUsersResponse,
|
|
11
|
+
} from '../../../../modules/pluto-github/runtime/shared/repo-users'
|
|
12
|
+
|
|
13
|
+
const { t } = usePlutoI18n()
|
|
14
|
+
const errorText = usePlutoErrorText()
|
|
15
|
+
const auth = usePlutoAuth()
|
|
16
|
+
|
|
17
|
+
useHead({ title: () => t('pluto.github.users.title') })
|
|
18
|
+
|
|
19
|
+
const answer = ref<PlutoGithubUsersResponse | null>(null)
|
|
20
|
+
const status = ref<'loading' | 'ready' | 'error'>('loading')
|
|
21
|
+
const loadError = ref('')
|
|
22
|
+
|
|
23
|
+
async function load() {
|
|
24
|
+
status.value = 'loading'
|
|
25
|
+
try {
|
|
26
|
+
answer.value = await $fetch<PlutoGithubUsersResponse>(
|
|
27
|
+
'/api/_pluto/github/users'
|
|
28
|
+
)
|
|
29
|
+
status.value = 'ready'
|
|
30
|
+
} catch (error) {
|
|
31
|
+
loadError.value = errorText(error)
|
|
32
|
+
status.value = 'error'
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
onMounted(load)
|
|
36
|
+
|
|
37
|
+
const repo = computed(
|
|
38
|
+
() =>
|
|
39
|
+
answer.value?.repo ||
|
|
40
|
+
String(
|
|
41
|
+
(useRuntimeConfig().public.plutoGithub as { repo?: string } | undefined)
|
|
42
|
+
?.repo ?? ''
|
|
43
|
+
)
|
|
44
|
+
)
|
|
45
|
+
const accessUrl = computed(() =>
|
|
46
|
+
repo.value ? `https://github.com/${repo.value}/settings/access` : ''
|
|
47
|
+
)
|
|
48
|
+
const self = computed(() => auth.user.value?.username?.toLowerCase() ?? '')
|
|
49
|
+
const users = computed(() => answer.value?.users ?? [])
|
|
50
|
+
// The i18n key of the reason why the list is empty.
|
|
51
|
+
const REASON_KEYS = {
|
|
52
|
+
'no-app': 'noApp',
|
|
53
|
+
'app-error': 'appError',
|
|
54
|
+
'no-repo': 'noRepo',
|
|
55
|
+
} as const
|
|
56
|
+
const reasonKey = computed(() => REASON_KEYS[answer.value?.reason ?? 'no-repo'])
|
|
57
|
+
|
|
58
|
+
const columns = computed<TableColumn<PlutoGithubRepoUser>[]>(() => [
|
|
59
|
+
{ id: 'user', header: t('pluto.github.users.columns.user') },
|
|
60
|
+
{ id: 'role', header: t('pluto.github.users.columns.role') },
|
|
61
|
+
{ id: 'access', header: t('pluto.github.users.columns.access') },
|
|
62
|
+
])
|
|
63
|
+
</script>
|
|
64
|
+
|
|
65
|
+
<template>
|
|
66
|
+
<main>
|
|
67
|
+
<AdminView>
|
|
68
|
+
<h1 class="text-3xl font-bold lg:text-4xl">
|
|
69
|
+
{{ t('pluto.github.users.title') }}
|
|
70
|
+
</h1>
|
|
71
|
+
|
|
72
|
+
<div class="flex flex-wrap items-start justify-between gap-4">
|
|
73
|
+
<p class="text-muted max-w-2xl text-sm">
|
|
74
|
+
{{ t('pluto.github.users.description', { repo: repo || 'GitHub' }) }}
|
|
75
|
+
</p>
|
|
76
|
+
<UButton
|
|
77
|
+
v-if="accessUrl"
|
|
78
|
+
:label="t('pluto.github.users.manage')"
|
|
79
|
+
:to="accessUrl"
|
|
80
|
+
color="neutral"
|
|
81
|
+
icon="i-pluto-github"
|
|
82
|
+
target="_blank"
|
|
83
|
+
trailing-icon="i-lucide-external-link"
|
|
84
|
+
variant="outline"
|
|
85
|
+
/>
|
|
86
|
+
</div>
|
|
87
|
+
|
|
88
|
+
<UAlert
|
|
89
|
+
v-if="answer?.restricted"
|
|
90
|
+
:description="t('pluto.github.users.restricted')"
|
|
91
|
+
color="info"
|
|
92
|
+
icon="i-lucide-filter"
|
|
93
|
+
variant="subtle"
|
|
94
|
+
/>
|
|
95
|
+
|
|
96
|
+
<!-- Loading -->
|
|
97
|
+
<div
|
|
98
|
+
v-if="status === 'loading'"
|
|
99
|
+
class="flex flex-col gap-y-3"
|
|
100
|
+
aria-busy="true"
|
|
101
|
+
>
|
|
102
|
+
<USkeleton v-for="row in 3" :key="row" class="h-12 w-full" />
|
|
103
|
+
</div>
|
|
104
|
+
|
|
105
|
+
<!-- Error -->
|
|
106
|
+
<UAlert
|
|
107
|
+
v-else-if="status === 'error'"
|
|
108
|
+
:title="t('pluto.github.users.error')"
|
|
109
|
+
:description="loadError"
|
|
110
|
+
:actions="[
|
|
111
|
+
{
|
|
112
|
+
label: t('pluto.common.tryAgain'),
|
|
113
|
+
color: 'neutral',
|
|
114
|
+
variant: 'outline',
|
|
115
|
+
onClick: load,
|
|
116
|
+
},
|
|
117
|
+
]"
|
|
118
|
+
color="error"
|
|
119
|
+
icon="i-lucide-circle-x"
|
|
120
|
+
variant="subtle"
|
|
121
|
+
/>
|
|
122
|
+
|
|
123
|
+
<!-- Local mode with no GitHub App or an App that cannot read, or no repo -->
|
|
124
|
+
<UAlert
|
|
125
|
+
v-else-if="answer?.reason"
|
|
126
|
+
:title="t(`pluto.github.users.reason.${reasonKey}.title`)"
|
|
127
|
+
:description="t(`pluto.github.users.reason.${reasonKey}.description`)"
|
|
128
|
+
:actions="
|
|
129
|
+
answer.reason !== 'no-repo'
|
|
130
|
+
? [
|
|
131
|
+
{
|
|
132
|
+
label: t(`pluto.github.users.reason.${reasonKey}.action`),
|
|
133
|
+
to: '/admin/settings/github',
|
|
134
|
+
color: 'neutral',
|
|
135
|
+
variant: 'outline',
|
|
136
|
+
},
|
|
137
|
+
]
|
|
138
|
+
: []
|
|
139
|
+
"
|
|
140
|
+
color="neutral"
|
|
141
|
+
icon="i-lucide-info"
|
|
142
|
+
variant="subtle"
|
|
143
|
+
/>
|
|
144
|
+
|
|
145
|
+
<UEmpty
|
|
146
|
+
v-else-if="users.length === 0"
|
|
147
|
+
:title="t('pluto.github.users.empty')"
|
|
148
|
+
icon="i-lucide-users"
|
|
149
|
+
/>
|
|
150
|
+
|
|
151
|
+
<UTable
|
|
152
|
+
v-else
|
|
153
|
+
:data="users"
|
|
154
|
+
:columns="columns"
|
|
155
|
+
class="border-default rounded-lg border"
|
|
156
|
+
>
|
|
157
|
+
<template #user-cell="{ row }">
|
|
158
|
+
<div class="flex min-w-0 items-center gap-3">
|
|
159
|
+
<UAvatar
|
|
160
|
+
:src="row.original.avatarUrl || undefined"
|
|
161
|
+
:alt="row.original.login"
|
|
162
|
+
size="sm"
|
|
163
|
+
/>
|
|
164
|
+
<ULink
|
|
165
|
+
:to="row.original.url"
|
|
166
|
+
class="text-highlighted truncate font-medium"
|
|
167
|
+
target="_blank"
|
|
168
|
+
>
|
|
169
|
+
{{ row.original.login }}
|
|
170
|
+
</ULink>
|
|
171
|
+
<UBadge
|
|
172
|
+
v-if="row.original.login.toLowerCase() === self"
|
|
173
|
+
:label="t('pluto.github.users.you')"
|
|
174
|
+
color="primary"
|
|
175
|
+
size="sm"
|
|
176
|
+
variant="subtle"
|
|
177
|
+
/>
|
|
178
|
+
</div>
|
|
179
|
+
</template>
|
|
180
|
+
|
|
181
|
+
<template #role-cell="{ row }">
|
|
182
|
+
<UBadge
|
|
183
|
+
:label="t(`pluto.github.users.roles.${row.original.role}`)"
|
|
184
|
+
:color="row.original.role === 'write' ? 'neutral' : 'primary'"
|
|
185
|
+
variant="subtle"
|
|
186
|
+
/>
|
|
187
|
+
</template>
|
|
188
|
+
|
|
189
|
+
<template #access-cell="{ row }">
|
|
190
|
+
<div class="flex flex-col items-start gap-1">
|
|
191
|
+
<span class="text-sm">
|
|
192
|
+
{{
|
|
193
|
+
row.original.role === 'write'
|
|
194
|
+
? t('pluto.github.users.access.editor')
|
|
195
|
+
: t('pluto.github.users.access.admin')
|
|
196
|
+
}}
|
|
197
|
+
</span>
|
|
198
|
+
<UBadge
|
|
199
|
+
v-if="!row.original.canSignIn"
|
|
200
|
+
:label="t('pluto.github.users.blocked')"
|
|
201
|
+
color="warning"
|
|
202
|
+
size="sm"
|
|
203
|
+
variant="subtle"
|
|
204
|
+
/>
|
|
205
|
+
</div>
|
|
206
|
+
</template>
|
|
207
|
+
</UTable>
|
|
208
|
+
|
|
209
|
+
<p class="text-muted text-xs">
|
|
210
|
+
{{ t('pluto.github.users.footnote') }}
|
|
211
|
+
</p>
|
|
212
|
+
</AdminView>
|
|
213
|
+
</main>
|
|
214
|
+
</template>
|