Continue frontend building

This commit is contained in:
2025-07-13 12:03:43 +02:00
parent 6faed0dbd4
commit 746f809d28
44 changed files with 2057 additions and 632 deletions

View File

@ -1,28 +0,0 @@
<template>
<div class="max-w-2xl mx-auto">
<h1 class="text-2xl mb-4">Clients</h1>
<ul class="space-y-2">
<li v-for="client in clients" :key="client.id">
<strong>{{ client.name }}</strong>
<span v-if="client.description">({{ client.description }})</span>
</li>
</ul>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import '@shoelace-style/shoelace/dist/components/tree-item/tree-item.js'
import '@shoelace-style/shoelace/dist/components/tree/tree.js'
import { SshecretAdmin } from '@/client/sdk.gen'
import MasterDetail from '@/components/layout/MasterDetail.vue'
const clients = ref<any[]>([])
onMounted(async () => {
const response = await SshecretAdmin.getClientsApiV1ClientsGet()
clients.value = response.data
})
</script>

View File

@ -33,7 +33,7 @@
</sl-input>
</div>
</div>
<div class="flex flex-col h-full min-h-0">
<div id="client-tree-items" class="flex flex-col h-full min-h-0">
<div class="flex-1 overflow-y-auto">
<sl-tree class="w-full" @sl-selection-change="itemSelected" v-if="treeState.clients">
<template v-for="client in treeState.clients.clients">
@ -61,74 +61,21 @@
</span>
<div class="inline-flex mt-2 xs:mt-0">
<nav aria-label="Page navigation">
<ul class="flex items-center -space-x-px h-8 text-sm">
<li>
<button
type="button"
class="flex items-center justify-center px-3 h-8 ms-0 leading-tight text-gray-500 bg-white border border-e-0 border-gray-300 rounded-s-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
:disabled="pageNum <= 1"
@click="prevPage"
>
<span class="sr-only">Previous</span>
<svg
class="w-2.5 h-2.5 rtl:rotate-180"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 6 10"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 1 1 5l4 4"
/>
</svg>
</button>
</li>
<li v-for="n in totalPages">
<button
class="z-10 flex items-center justify-center px-3 h-8 leading-tight text-blue-600 border border-blue-300 bg-blue-50 hover:bg-blue-100 hover:text-blue-700 dark:border-gray-700 dark:bg-gray-700 dark:text-white"
disabled
v-if="n === pageNum"
>
{{ n }}
</button>
<button
class="flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
@click="pageNum = n"
v-else
>
{{ n }}
</button>
</li>
<li>
<button
type="button"
class="flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 rounded-e-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
@click="nextPage"
:disabled="pageNum >= totalPages"
>
<span class="sr-only">Next</span>
<svg
class="w-2.5 h-2.5 rtl:rotate-180"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 6 10"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m1 9 4-4-4-4"
/>
</svg>
</button>
</li>
</ul>
<PageNumbers
@next="nextPage"
@previous="prevPage"
@goto="goToPage"
:pageNum="pageNum"
:totalPages="totalPages"
>
<template #previous>
<sl-icon name="chevron-left" slot="prefix"></sl-icon>
</template>
<template #next>
<sl-icon name="chevron-right" slot="prefix"></sl-icon>
</template>
</PageNumbers>
</nav>
</div>
</div>
@ -144,13 +91,6 @@
import { computed, ref, reactive, onMounted, watch } from 'vue'
import type { Ref } from 'vue'
import '@shoelace-style/shoelace/dist/components/tree-item/tree-item.js'
import '@shoelace-style/shoelace/dist/components/tree/tree.js'
import '@shoelace-style/shoelace/dist/components/icon-button/icon-button.js'
import '@shoelace-style/shoelace/dist/components/input/input.js'
import '@shoelace-style/shoelace/dist/components/icon/icon.js'
import '@shoelace-style/shoelace/dist/components/drawer/drawer.js'
import { usePagination } from '@/composables/usePagination'
import { SshecretAdmin } from '@/client/sdk.gen'
@ -159,16 +99,16 @@ import type { Client, ClientCreate } from '@/client/types.gen'
import { useTreeState } from '@/store/useTreeState'
import MasterDetail from '@/components/layout/MasterDetail.vue'
import ClientTreeItem from '@/components/clients/ClientTreeItem.vue'
import ClientSecretTreeItem from '@/components/clients/ClientSecretTreeItem.vue'
import ClientForm from '@/components/clients/ClientForm.vue'
import PageNumbers from '@/components/common/PageNumbers.vue'
import { useDebounce } from '@/composables/useDebounce'
const treeState = useTreeState()
const clientsPerPage = 20
const totalClients = computed(() => treeState.clients?.total_clients)
const totalClients = computed(() => treeState.clients?.total_results)
const clients = computed(() => treeState.clients.clients)
const selectedClient = ref<Client | null>(null)
@ -181,16 +121,12 @@ const clientQuery = ref('')
const debouncedQuery = useDebounce(clientQuery, 300)
const { pageNum, offset, firstResult, lastResult, totalPages, nextPage, prevPage } = usePagination(
totalClients,
clientsPerPage,
)
const { pageNum, offset, firstResult, lastResult, totalPages, nextPage, prevPage, goToPage } =
usePagination(totalClients, clientsPerPage)
async function loadClients() {
if (clientQuery.value) {
console.log('Search term: ', clientQuery.value)
await treeState.queryClients(clientQuery.value, offset.value, clientsPerPage)
console.log(`Got ${fetchedClients} results`)
} else {
await treeState.loadClients(offset.value, clientsPerPage)
}
@ -210,7 +146,7 @@ function itemSelected(event: Event) {
} else {
const el = event.detail.selection[0] as HTMLElement
const childType = el.dataset.type
if (childType == 'client') {
if (childType === 'client') {
const clientId = el.dataset.clientId
treeState.selectClient(clientId)
} else if (childType == 'secret') {

View File

@ -1,284 +0,0 @@
<template>
<MasterDetail>
<template #master>
<div class="flex flex-col h-full min-h-0">
<div class="tree-header mb-2 grid grid-cols-2 place-content-between">
<h1 class="text-lg font-semibold text-gray-900 dark:text-white">Client List</h1>
<div class="flex">
<div class="flex w-full justify-end">
<sl-icon-button
name="plus-square"
label="Add Client"
@click="createDrawerOpen = !createDrawerOpen"
></sl-icon-button>
</div>
</div>
<div class="col-span-full">
<label
for="client-search"
name="client-search"
class="mb-2 text-xs font-medium text-gray-900 sr-only dark:text-white"
>
Search
</label>
<sl-input type="search" size="small" placeholder="search" clearable>
<template v-slot:prefix>
<sl-icon name="search" ></sl-icon>
</template>
</sl-input>
</div>
</div>
<div class="flex flex-col h-full min-h-0">
<div class="flex-1 overflow-y-auto">
<sl-tree class="w-full" @sl-selection-change="itemSelected">
<template v-for="client in clients">
<ClientTreeItem :id="client.id" :name="client.name" :selected="false">
<template v-for="secret in client.secrets">
<ClientSecretTreeItem :parent_id="client.id" :name="secret" />
</template>
</ClientTreeItem>
</template>
</sl-tree>
</div>
<div
class="shrink-0 mt-4 pt-2 border-t border-gray-100 dark:border-gray-700 bg-white dark:bg-gray-800"
v-if="totalPages > 1"
>
<div class="mt-4 text-center flex items-center flex-col">
<span class="text-sm text-gray-700 dark:text-gray-400">
Showing
<span class="font-semibold text-gray-900 dark:text-white">{{ firstResult }}</span>
to
<span class="font-semibold text-gray-900 dark:text-white">{{ lastResult }}</span>
of
<span class="font-semibold text-gray-900 dark:text-white">{{ totalClients }}</span>
Entries
</span>
<div class="inline-flex mt-2 xs:mt-0">
<nav aria-label="Page navigation">
<ul class="flex items-center -space-x-px h-8 text-sm">
<li>
<button
type="button"
class="flex items-center justify-center px-3 h-8 ms-0 leading-tight text-gray-500 bg-white border border-e-0 border-gray-300 rounded-s-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
:disabled="pageNum <= 1"
@click="prevPage"
>
<span class="sr-only">Previous</span>
<svg
class="w-2.5 h-2.5 rtl:rotate-180"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 6 10"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="M5 1 1 5l4 4"
/>
</svg>
</button>
</li>
<li v-for="n in totalPages">
<button
class="z-10 flex items-center justify-center px-3 h-8 leading-tight text-blue-600 border border-blue-300 bg-blue-50 hover:bg-blue-100 hover:text-blue-700 dark:border-gray-700 dark:bg-gray-700 dark:text-white"
disabled
v-if="n === pageNum"
>
{{ n }}
</button>
<button
class="flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
@click="pageNum = n"
v-else
>
{{ n }}
</button>
</li>
<li>
<button
type="button"
class="flex items-center justify-center px-3 h-8 leading-tight text-gray-500 bg-white border border-gray-300 rounded-e-lg hover:bg-gray-100 hover:text-gray-700 dark:bg-gray-800 dark:border-gray-700 dark:text-gray-400 dark:hover:bg-gray-700 dark:hover:text-white"
@click="nextPage"
:disabled="pageNum >= totalPages"
>
<span class="sr-only">Next</span>
<svg
class="w-2.5 h-2.5 rtl:rotate-180"
aria-hidden="true"
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 6 10"
>
<path
stroke="currentColor"
stroke-linecap="round"
stroke-linejoin="round"
stroke-width="2"
d="m1 9 4-4-4-4"
/>
</svg>
</button>
</li>
</ul>
</nav>
</div>
</div>
</div>
</div>
</div>
</template>
<template #detail v-if="treeState.selected">
<ClientDetail
:client="treeState.client"
:key="treeState.client.id"
v-if="treeState.item_type == 'client'"
@update="updateClient"
@deleted="clientDeleted"
/>
<SecretDetail
:secret_name="treeState.secret_name"
:key="treeState.secret_name"
v-if="treeState.item_type == 'secret'"
/>
</template>
</MasterDetail>
<sl-drawer label="Create Client" :open="createDrawerOpen" @sl-hide="createDrawerOpen = false">
<ClientForm @submit="createClient" @cancel="createDrawerOpen = false" :key="createFormKey" />
</sl-drawer>
</template>
<script setup lang="ts">
import { computed, ref, reactive, onMounted, watch } from 'vue'
import type { Ref } from 'vue'
import '@shoelace-style/shoelace/dist/components/tree-item/tree-item.js'
import '@shoelace-style/shoelace/dist/components/tree/tree.js'
import '@shoelace-style/shoelace/dist/components/icon-button/icon-button.js'
import '@shoelace-style/shoelace/dist/components/input/input.js'
import '@shoelace-style/shoelace/dist/components/icon/icon.js'
import '@shoelace-style/shoelace/dist/components/drawer/drawer.js'
import { usePagination } from '@/composables/usePagination'
import { SshecretAdmin } from '@/client/sdk.gen'
import type { Client, ClientCreate } from '@/client/types.gen'
import { useTreeState } from '@/store/useTreeState'
import MasterDetail from '@/components/layout/MasterDetail.vue'
import ClientTreeItem from '@/components/clients/ClientTreeItem.vue'
import ClientSecretTreeItem from '@/components/clients/ClientSecretTreeItem.vue'
import ClientDetail from '@/components/clients/ClientDetail.vue'
import SecretDetail from '@/components/secrets/SecretDetail.vue'
import ClientForm from '@/components/clients/ClientForm.vue'
const clientsPerPage = 20
const totalClients = ref<number>(0)
const clients = ref<Client[]>([])
interface TreeState {
selected: boolean
item_type?: string
secret_name?: string
client?: Client
}
const treeState = ref<TreeState>({ selected: false })
const selectedClient = ref<Client | null>(null)
const selectedSecret = ref<string | null>(null)
const createFormKey = ref<number>(0)
const createDrawerOpen = ref<boolean>(false)
const { pageNum, offset, firstResult, lastResult, totalPages, nextPage, prevPage } = usePagination(
totalClients,
20,
)
async function loadClients() {
const response = await SshecretAdmin.queryClientsApiV1QueryClientsGet({
query: {
offset: offset.value,
limit: clientsPerPage,
},
})
clients.value = response.data.clients
totalClients.value = response.data.total_results
}
function updateClient(updated: Client) {
const index = clients.value.findIndex((c) => c.name === updated.name)
console.log(`UpdateClient fired: ${updated.name} => ${index}`)
if (index >= 0) {
clients.value[index] = updated
}
}
function itemSelected(event: Event) {
if (event.detail.selection.length == 0) {
treeState.value.selected = false
treeState.value.client = null
treeState.value.secret_name = null
treeState.value.item_type = null
} else {
console.log('Something else was selected')
treeState.value.selected = true
const el = event.detail.selection[0] as HTMLElement
const childType = el.dataset.type
if (childType == 'client') {
const clientId = el.dataset.clientId
console.log(`Selected client ${clientId}`)
const targetClient = clients.value.find((client) => client.id === clientId)
treeState.value.item_type = 'client'
treeState.value.client = targetClient
treeState.value.secret_name = null
} else if (childType == 'secret') {
const secretName = el.dataset.name
treeState.value.item_type = 'secret'
treeState.value.secret_name = secretName
treeState.value.client = null
}
}
}
async function createClient(data: ClientCreate) {
const response = await SshecretAdmin.createClientApiV1ClientsPost({ body: data })
console.log(response.data)
clients.value.unshift(response.data)
totalClients.value += 1
createDrawerOpen.value = false
createFormKey.value += 1
treeState.value.selected = true
treeState.value.item_type = 'secret'
treeState.value.client = response.data
}
async function clientDeleted(id: string) {
const index = clients.value.findIndex((c) => c.id === id)
console.log(`Client Deleted event received: ID: ${id} => ${index}`)
if (index >= 0) {
clients.value.splice(index, 1)
treeState.value.selected = false
treeState.value.item_type = null
treeState.value.client = null
await loadClients()
}
}
onMounted(loadClients)
watch([offset, pageNum], loadClients)
</script>
<style scoped>
sl-input::part(prefix) {
margin-left: 1rem;
}
sl-input::part(base) {
background-color: var(--color-gray-50);
}
</style>