refactored install dialog

This commit is contained in:
2025-10-07 00:41:21 +02:00
parent 225835e5d1
commit c8c3a5c73f
44 changed files with 1426 additions and 730 deletions

View File

@ -5,166 +5,249 @@
@confirm="onConfirm"
>
<template #title>
<i18n-t
keypath="question"
tag="p"
>
<template #extension>
<span class="font-bold text-primary">{{ manifest?.name }}</span>
</template>
</i18n-t>
{{ t('title') }}
</template>
<div class="flex flex-col">
<nav
class="tabs tabs-bordered"
aria-label="Tabs"
role="tablist"
aria-orientation="horizontal"
>
<button
v-show="manifest?.permissions?.database"
id="tabs-basic-item-1"
type="button"
class="tab active-tab:tab-active active"
data-tab="#tabs-basic-1"
aria-controls="tabs-basic-1"
role="tab"
aria-selected="true"
>
{{ t('database') }}
</button>
<button
v-show="manifest?.permissions?.filesystem"
id="tabs-basic-item-2"
type="button"
class="tab active-tab:tab-active"
data-tab="#tabs-basic-2"
aria-controls="tabs-basic-2"
role="tab"
aria-selected="false"
>
{{ t('filesystem') }}
</button>
<button
v-show="manifest?.permissions?.http"
id="tabs-basic-item-3"
type="button"
class="tab active-tab:tab-active"
data-tab="#tabs-basic-3"
aria-controls="tabs-basic-3"
role="tab"
aria-selected="false"
>
{{ t('http') }}
</button>
</nav>
<template #body>
<div class="flex flex-col gap-6">
<!-- Extension Info -->
<UCard>
<div class="flex items-start gap-4">
<div
v-if="preview?.manifest.icon"
class="w-16 h-16 flex-shrink-0"
>
<UIcon
:name="preview.manifest.icon"
class="w-full h-full"
/>
</div>
<div class="flex-1">
<h3 class="text-xl font-bold">
{{ preview?.manifest.name }}
</h3>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ t('version') }}: {{ preview?.manifest.version }}
</p>
<p
v-if="preview?.manifest.author"
class="text-sm text-gray-500 dark:text-gray-400"
>
{{ t('author') }}: {{ preview.manifest.author }}
</p>
<p
v-if="preview?.manifest.description"
class="text-sm mt-2"
>
{{ preview.manifest.description }}
</p>
<div class="mt-3 min-h-40">
<div
id="tabs-basic-1"
role="tabpanel"
aria-labelledby="tabs-basic-item-1"
>
<HaexExtensionManifestPermissionsDatabase
:database="permissions?.database"
/>
</div>
<div
id="tabs-basic-2"
class="hidden"
role="tabpanel"
aria-labelledby="tabs-basic-item-2"
>
<HaexExtensionManifestPermissionsFilesystem
:filesystem="permissions?.filesystem"
/>
</div>
<div
id="tabs-basic-3"
class="hidden"
role="tabpanel"
aria-labelledby="tabs-basic-item-3"
>
<HaexExtensionManifestPermissionsHttp :http="permissions?.http" />
<!-- Signature Verification -->
<UBadge
:color="preview?.is_valid_signature ? 'success' : 'error'"
variant="subtle"
class="mt-2"
>
<template #leading>
<UIcon
:name="
preview?.is_valid_signature
? 'i-heroicons-shield-check'
: 'i-heroicons-shield-exclamation'
"
/>
</template>
{{
preview?.is_valid_signature
? t('signature.valid')
: t('signature.invalid')
}}
</UBadge>
</div>
</div>
</UCard>
<!-- Permissions Section -->
<div class="flex flex-col gap-4">
<h4 class="text-lg font-semibold">
{{ t('permissions.title') }}
</h4>
<UAccordion
:items="permissionAccordionItems"
:ui="{ root: 'flex flex-col gap-2' }"
>
<template #database>
<div
v-if="databasePermissions"
class="pb-4"
>
<HaexExtensionPermissionList
v-model="databasePermissions"
:title="t('permissions.database')"
/>
</div>
</template>
<template #filesystem>
<div
v-if="filesystemPermissions"
class="pb-4"
>
<HaexExtensionPermissionList
v-model="filesystemPermissions"
:title="t('permissions.filesystem')"
/>
</div>
</template>
<template #http>
<div
v-if="httpPermissions"
class="pb-4"
>
<HaexExtensionPermissionList
v-model="httpPermissions"
:title="t('permissions.http')"
/>
</div>
</template>
<template #shell>
<div
v-if="shellPermissions"
class="pb-4"
>
<HaexExtensionPermissionList
v-model="shellPermissions"
:title="t('permissions.shell')"
/>
</div>
</template>
</UAccordion>
</div>
</div>
</div>
</template>
</UiDialogConfirm>
</template>
<script setup lang="ts">
import type { IHaexHubExtensionManifest } from '~/types/haexhub'
import type { ExtensionPreview } from '~~/src-tauri/bindings/ExtensionPreview'
const { t } = useI18n()
const open = defineModel<boolean>('open', { default: false })
const { manifest } = defineProps<{
manifest?: IHaexHubExtensionManifest | null
const props = defineProps<{
preview?: ExtensionPreview | null
}>()
const permissions = computed(() => ({
database: {
read: manifest?.permissions.database?.read?.map((read) => ({
[read]: true,
})),
write: manifest?.permissions.database?.read?.map((write) => ({
[write]: true,
})),
create: manifest?.permissions.database?.read?.map((create) => ({
[create]: true,
})),
const databasePermissions = ref(
props.preview?.editable_permissions?.database || [],
)
const filesystemPermissions = ref(
props.preview?.editable_permissions?.filesystem || [],
)
const httpPermissions = ref(props.preview?.editable_permissions?.http || [])
const shellPermissions = ref(props.preview?.editable_permissions?.shell || [])
// Watch for preview changes
watch(
() => props.preview,
(newPreview) => {
if (newPreview?.editable_permissions) {
databasePermissions.value = newPreview.editable_permissions.database || []
filesystemPermissions.value =
newPreview.editable_permissions.filesystem || []
httpPermissions.value = newPreview.editable_permissions.http || []
shellPermissions.value = newPreview.editable_permissions.shell || []
}
},
{ immediate: true },
)
filesystem: {
read: manifest?.permissions.filesystem?.read?.map((read) => ({
[read]: true,
})),
write: manifest?.permissions.filesystem?.write?.map((write) => ({
[write]: true,
})),
},
const permissionAccordionItems = computed(() => {
const items = []
http: manifest?.permissions.http?.map((http) => ({
[http]: true,
})),
}))
if (databasePermissions.value?.length) {
items.push({
label: t('permissions.database'),
icon: 'i-heroicons-circle-stack',
slot: 'database',
defaultOpen: true,
})
}
if (filesystemPermissions.value?.length) {
items.push({
label: t('permissions.filesystem'),
icon: 'i-heroicons-folder',
slot: 'filesystem',
})
}
if (httpPermissions.value?.length) {
items.push({
label: t('permissions.http'),
icon: 'i-heroicons-globe-alt',
slot: 'http',
})
}
if (shellPermissions.value?.length) {
items.push({
label: t('permissions.shell'),
icon: 'i-heroicons-command-line',
slot: 'shell',
})
}
return items
})
watch(permissions, () => console.log('permissions', permissions.value))
const emit = defineEmits(['deny', 'confirm'])
const onDeny = () => {
open.value = false
console.log('onDeny open', open.value)
emit('deny')
}
const onConfirm = () => {
open.value = false
console.log('onConfirm open', open.value)
emit('confirm')
emit('confirm', {
database: databasePermissions.value,
filesystem: filesystemPermissions.value,
http: httpPermissions.value,
shell: shellPermissions.value,
})
}
</script>
<i18n lang="json">
{
"de": {
"title": "Erweiterung hinzufügen",
"question": "Erweiterung {extension} hinzufügen?",
"confirm": "Bestätigen",
"deny": "Ablehnen",
"database": "Datenbank",
"http": "Internet",
"filesystem": "Dateisystem"
},
"en": {
"title": "Confirm Permission",
"question": "Add Extension {extension}?",
"confirm": "Confirm",
"deny": "Deny",
"database": "Database",
"http": "Internet",
"filesystem": "Filesystem"
}
}
<i18n lang="yaml">
de:
title: Erweiterung installieren
version: Version
author: Autor
signature:
valid: Signatur verifiziert
invalid: Signatur ungültig
permissions:
title: Berechtigungen
database: Datenbank
filesystem: Dateisystem
http: Internet
shell: Terminal
en:
title: Install Extension
version: Version
author: Author
signature:
valid: Signature verified
invalid: Invalid signature
permissions:
title: Permissions
database: Database
filesystem: Filesystem
http: Internet
shell: Terminal
</i18n>

View File

@ -0,0 +1,128 @@
<template>
<div
v-if="menuEntry"
class="flex items-center justify-between gap-4 p-3 rounded-lg border border-base-300 bg-base-100"
>
<div class="flex-1 min-w-0">
<div class="font-medium truncate">
{{ modelValue.target }}
</div>
<div
v-if="modelValue.operation"
class="text-sm text-gray-500 dark:text-gray-400"
>
{{ t(`operation.${modelValue.operation}`) }}
</div>
</div>
<div class="flex items-center gap-2">
<!-- Status Selector -->
<USelectMenu
v-model="menuEntry"
:items="statusOptions"
value-attribute="value"
class="w-44"
>
<template #leading>
<UIcon
:name="getStatusIcon(menuEntry?.value)"
:class="getStatusColor(menuEntry?.value)"
/>
</template>
<template #item-leading="{ item }">
<UIcon
:name="getStatusIcon(item?.value)"
:class="getStatusColor(item?.value)"
/>
</template>
</USelectMenu>
</div>
</div>
</template>
<script setup lang="ts">
import type { PermissionEntry } from '~~/src-tauri/bindings/PermissionEntry'
import type { PermissionStatus } from '~~/src-tauri/bindings/PermissionStatus'
const permissionEntry = defineModel<PermissionEntry>({ required: true })
const menuEntry = computed({
get: () =>
statusOptions.value.find(
(option) => option.value == permissionEntry.value.status,
),
set(newStatus) {
const status =
statusOptions.value.find((option) => option.value == newStatus?.value)
?.value || 'denied'
if (isPermissionStatus(status)) {
permissionEntry.value.status = status
} else {
permissionEntry.value.status = 'denied'
}
},
})
const { t } = useI18n()
const isPermissionStatus = (value: string): value is PermissionStatus => {
return ['ask', 'granted', 'denied'].includes(value)
}
const statusOptions = computed(() => [
{
value: 'granted',
label: t('status.granted'),
icon: 'i-heroicons-check-circle',
color: 'text-green-500',
},
{
value: 'ask',
label: t('status.ask'),
icon: 'i-heroicons-question-mark-circle',
color: 'text-yellow-500',
},
{
value: 'denied',
label: t('status.denied'),
icon: 'i-heroicons-x-circle',
color: 'text-red-500',
},
])
const getStatusIcon = (status: string) => {
const option = statusOptions.value.find((o) => o.value === status)
return option?.icon || 'i-heroicons-question-mark-circle'
}
const getStatusColor = (status: string) => {
const option = statusOptions.value.find((o) => o.value === status)
return option?.color || 'text-gray-500'
}
</script>
<i18n lang="yaml">
de:
status:
granted: Erlaubt
ask: Nachfragen
denied: Verweigert
operation:
read: Lesen
write: Schreiben
readWrite: Lesen & Schreiben
request: Anfrage
execute: Ausführen
en:
status:
granted: Granted
ask: Ask
denied: Denied
operation:
read: Read
write: Write
readWrite: Read & Write
request: Request
execute: Execute
</i18n>

View File

@ -0,0 +1,30 @@
<template>
<div
v-if="modelValue?.length"
class="flex flex-col gap-2"
>
<h5
v-if="title"
class="text-sm font-semibold text-gray-700 dark:text-gray-300"
>
{{ title }}
</h5>
<div class="flex flex-col gap-2">
<HaexExtensionPermissionItem
v-for="(perm, index) in modelValue"
:key="perm.target"
v-model="modelValue[index]!"
/>
</div>
</div>
</template>
<script setup lang="ts">
import type { PermissionEntry } from '~~/src-tauri/bindings/PermissionEntry'
defineProps<{
title?: string
}>()
const modelValue = defineModel<PermissionEntry[]>({ default: () => [] })
</script>

View File

@ -84,8 +84,6 @@ const filteredSlots = computed(() => {
)
})
watchImmediate(props, () => console.log('props', props))
const { isSmallScreen } = storeToRefs(useUiStore())
</script>