Continue frontend building
This commit is contained in:
@ -1,30 +1,93 @@
|
||||
<template>
|
||||
<MasterDetail>
|
||||
<template #master>
|
||||
<ClientTreeList />
|
||||
<sl-tab-group
|
||||
id="sideTabs"
|
||||
class="flex flex-col flex-1 h-full overflow-hidden master-pane-tabs"
|
||||
@sl-tab-show="tabSelected($event)"
|
||||
>
|
||||
<sl-tab slot="nav" panel="clients">Clients</sl-tab>
|
||||
<sl-tab slot="nav" panel="secrets">Secrets</sl-tab>
|
||||
<sl-tab slot="nav" panel="audit">Audit</sl-tab>
|
||||
<sl-tab-panel name="clients">
|
||||
<ClientTreeList />
|
||||
</sl-tab-panel>
|
||||
<sl-tab-panel name="secrets">
|
||||
<SecretTreeList />
|
||||
</sl-tab-panel>
|
||||
<sl-tab-panel name="audit">
|
||||
<AuditFilters />
|
||||
</sl-tab-panel>
|
||||
</sl-tab-group>
|
||||
</template>
|
||||
<template #detail v-if="treeState.selected">
|
||||
<ClientDetailView
|
||||
v-if="treeState.selected.objectType === SshecretObjectType.Client"
|
||||
:clientId="treeState.selected.id"
|
||||
:key="treeState.selected.id"
|
||||
/>
|
||||
<SecretDetailView
|
||||
v-else-if="treeState.selected.objectType === SshecretObjectType.ClientSecret"
|
||||
:secretName="treeState.selected.id"
|
||||
:parentId="null"
|
||||
:key="treeState.selected.id"
|
||||
/>
|
||||
<template #detail v-if="showAudit">
|
||||
<AuditView />
|
||||
</template>
|
||||
<template #detail v-else>
|
||||
<template v-if="treeState.selected">
|
||||
<ClientDetailView
|
||||
v-if="treeState.selected.objectType === SshecretObjectType.Client"
|
||||
:clientId="treeState.selected.id"
|
||||
:key="treeState.selected.id"
|
||||
/>
|
||||
<SecretDetailView
|
||||
v-else-if="treeState.selected.objectType === SshecretObjectType.ClientSecret"
|
||||
:secretName="treeState.selected.id"
|
||||
:parentId="null"
|
||||
:key="treeState.selected.id"
|
||||
/>
|
||||
<SecretGroupDetailView
|
||||
v-else-if="
|
||||
treeState.selected.objectType === SshecretObjectType.SecretGroup &&
|
||||
treeState.selected.id != 'ungrouped'
|
||||
"
|
||||
:groupPath="treeState.selected.id"
|
||||
:key="treeState.selected.id"
|
||||
/>
|
||||
</template>
|
||||
</template>
|
||||
</MasterDetail>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
import AuditView from '@/views/audit/AuditView.vue'
|
||||
import MasterDetail from '@/views/layout/MasterDetail.vue'
|
||||
import ClientTreeList from '@/views/Clients/ClientTreeList.vue'
|
||||
import ClientTreeList from '@/views/clients/ClientTreeList.vue'
|
||||
import SecretTreeList from '@/views/secrets/SecretTreeList.vue'
|
||||
import ClientDetailView from '@/views/clients/ClientDetailView.vue'
|
||||
import SecretDetailView from '@/views/secrets/SecretDetailView.vue'
|
||||
import SecretGroupDetailView from '@/views/secrets/SecretGroupDetailView.vue'
|
||||
|
||||
import AuditFilters from '@/components/audit/AuditFilters.vue'
|
||||
import { SshecretObjectType } from '@/api/types'
|
||||
import { useAuditFilterState } from '@/store/useAuditFilterState'
|
||||
import { useTreeState } from '@/store/useTreeState'
|
||||
|
||||
const treeState = useTreeState()
|
||||
|
||||
const auditFilterState = useAuditFilterState()
|
||||
|
||||
const showAudit = ref<{ boolean }>()
|
||||
|
||||
function tabSelected(tab) {
|
||||
const tabName = tab.detail.name
|
||||
if (tabName == 'audit') {
|
||||
console.log('Showing audit')
|
||||
treeState.showAudit = true
|
||||
showAudit.value = true
|
||||
} else {
|
||||
treeState.showAudit = false
|
||||
showAudit.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<style>
|
||||
sl-tab-group.master-pane-tabs::part(base),
|
||||
sl-tab-group.master-pane-tabs::part(body),
|
||||
sl-tab-group.master-pane-tabs sl-tab-panel::part(base),
|
||||
sl-tab-group.master-pane-tabs sl-tab-panel::part(body),
|
||||
sl-tab-group.master-pane-tabs sl-tab-panel {
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
||||
|
||||
22
packages/sshecret-frontend/src/views/audit/AuditView.vue
Normal file
22
packages/sshecret-frontend/src/views/audit/AuditView.vue
Normal file
@ -0,0 +1,22 @@
|
||||
<template>
|
||||
<template v-if="loaded">
|
||||
<AuditTable :auditFilter="auditFilter" />
|
||||
</template>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import AuditTable from '@/components/audit/AuditTable.vue'
|
||||
import type { AuditFilter } from '@/api/types'
|
||||
import type { GetAuditLogApiV1AuditGetData } from '@/client'
|
||||
import { useAuditFilterState } from '@/store/useAuditFilterState'
|
||||
const auditFilterState = useAuditFilterState()
|
||||
const auditFilter = ref<GetAuditLogApiV1AuditGetData['query']>({})
|
||||
|
||||
watch(auditFilterState, () => (auditFilter.value = auditFilterState.getFilter))
|
||||
const loaded = ref<{ boolean }>()
|
||||
|
||||
onMounted(() => {
|
||||
loaded.value = true
|
||||
auditFilter.value = auditFilterState.getFilter
|
||||
})
|
||||
</script>
|
||||
@ -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>
|
||||
@ -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') {
|
||||
|
||||
@ -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>
|
||||
@ -1,5 +1,5 @@
|
||||
<template>
|
||||
<navbar>
|
||||
<Navbar>
|
||||
<button
|
||||
id="sidebar-toggle"
|
||||
aria-expanded="true"
|
||||
@ -10,28 +10,26 @@
|
||||
>
|
||||
<sl-icon name="list" class="text-xl"></sl-icon>
|
||||
</button>
|
||||
</navbar>
|
||||
</Navbar>
|
||||
|
||||
<main id="content" class="flex-1 overflow-y-auto">
|
||||
<div>
|
||||
<div class="flex h-[calc(100vh-3.5rem)] overflow-hidden">
|
||||
<aside
|
||||
id="master-pane"
|
||||
:class="[
|
||||
'flex flex-col overflow-hidden lg:block lg:w-80 w-full shrink-0 border-r bg-white border-gray-200 p-4 dark:bg-gray-800 dark:border-gray-700',
|
||||
{ hidden: masterHidden },
|
||||
]"
|
||||
>
|
||||
<slot name="master" />
|
||||
</aside>
|
||||
<section id="detail-pane" class="flex-1 flex overflow-y-auto bg-white p-4 dark:bg-gray-800">
|
||||
<div class="flex flex-col w-full">
|
||||
<slot name="detail">
|
||||
<p class="p-4 text-gray-500 dark:text-gray-200">Select an item to view details</p>
|
||||
</slot>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
<div class="flex h-[calc(100vh-3.5rem)] overflow-hidden">
|
||||
<aside
|
||||
id="master-pane"
|
||||
:class="[
|
||||
'flex flex-col overflow-hidden lg:block lg:w-80 w-full shrink-0 border-r bg-white border-gray-200 p-4 dark:bg-gray-800 dark:border-gray-700',
|
||||
{ hidden: masterHidden },
|
||||
]"
|
||||
>
|
||||
<slot name="master" />
|
||||
</aside>
|
||||
<section id="detail-pane" class="flex-1 flex overflow-y-auto bg-white p-4 dark:bg-gray-800">
|
||||
<div class="flex flex-col w-full">
|
||||
<slot name="detail">
|
||||
<p class="p-4 text-gray-500 dark:text-gray-200">Select an item to view details</p>
|
||||
</slot>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
@ -40,6 +40,7 @@ async function updateSecretValue(value: string) {
|
||||
})
|
||||
if (props.parentId) {
|
||||
await treeState.refreshClient(props.parentId)
|
||||
await loadSecret()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -0,0 +1,28 @@
|
||||
<template>
|
||||
<GroupDetail :group="group" v-if="group" />
|
||||
<ClientSkeleton v-else />
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import { ref, watch, onMounted } from 'vue'
|
||||
import type { ClientSecretGroup } from '@/client'
|
||||
import { useTreeState } from '@/store/useTreeState'
|
||||
import ClientSkeleton from '@/components/clients/ClientSkeleton.vue'
|
||||
import GroupDetail from '@/components/secrets/GroupDetail.vue'
|
||||
|
||||
const treeState = useTreeState()
|
||||
const props = defineProps<{ groupPath: string }>()
|
||||
|
||||
const group = ref<ClientSecretGroup>()
|
||||
|
||||
async function loadGroup() {
|
||||
if (!props.groupPath) return
|
||||
group.value = await treeState.getGroup(props.groupPath)
|
||||
}
|
||||
onMounted(loadGroup)
|
||||
|
||||
watch(
|
||||
() => props.groupPath,
|
||||
() => loadGroup(),
|
||||
{ immediate: true },
|
||||
)
|
||||
</script>
|
||||
194
packages/sshecret-frontend/src/views/secrets/SecretTreeList.vue
Normal file
194
packages/sshecret-frontend/src/views/secrets/SecretTreeList.vue
Normal file
@ -0,0 +1,194 @@
|
||||
<template>
|
||||
<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">Secrets</h1>
|
||||
<div class="flex">
|
||||
<div class="flex w-full justify-end">
|
||||
<sl-dropdown>
|
||||
<sl-icon-button slot="trigger" name="plus-square" label="Add Secret"></sl-icon-button>
|
||||
<sl-menu>
|
||||
<sl-menu-item @click="createSecretDrawer = true">Create secret</sl-menu-item>
|
||||
<sl-menu-item @click="createGroupDrawer = true">
|
||||
<span v-if="currentPath">Create subgroup</span>
|
||||
<span v-else>Create group</span>
|
||||
</sl-menu-item>
|
||||
</sl-menu>
|
||||
</sl-dropdown>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-span-full">
|
||||
<!-- The search would have gone here... -->
|
||||
|
||||
</div>
|
||||
<div id="secret-tree-items" class="flex flex-col h-full min-h-0 col-span-full">
|
||||
<div class="flex-1 overflow-y-auto">
|
||||
<sl-tree class="w-full" @sl-selection-change="itemSelected" v-if="treeState.secretGroups">
|
||||
<template v-if="ungrouped">
|
||||
<SecretGroupTreeItem path="ungrouped" name="Ungrouped" groupPath="ungrouped">
|
||||
<template v-for="entry in ungrouped">
|
||||
<SecretGroupTreeEntry :name="entry.name" groupPath="ungrouped" />
|
||||
</template>
|
||||
</SecretGroupTreeItem>
|
||||
</template>
|
||||
<template v-if="secretGroups">
|
||||
<SecretGroup v-for="group in secretGroups" :group="group" />
|
||||
</template>
|
||||
</sl-tree>
|
||||
</div>
|
||||
<!-- pagination would go here -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<Drawer label="Create Group" :open="createGroupDrawer" @hide="createGroupDrawer = false">
|
||||
<AddGroup
|
||||
:parent="currentPath"
|
||||
@submit="createGroup"
|
||||
@cancel="cancelCreateGroup"
|
||||
:key="drawerKey"
|
||||
/>
|
||||
</Drawer>
|
||||
<Drawer label="Create Secret" :open="createSecretDrawer" @hide="createSecretDrawer = false">
|
||||
<SecretForm
|
||||
:key="createDrawerKey"
|
||||
:group="createSecretParent"
|
||||
@submit="createSecret"
|
||||
@cancel="createSecretDrawer = false"
|
||||
/>
|
||||
</Drawer>
|
||||
</template>
|
||||
<script setup lang="ts">
|
||||
import type { SshecretObject } from '@/api/types'
|
||||
import type { SecretCreate } from '@/client'
|
||||
import { computed, ref, reactive, onMounted, watch } from 'vue'
|
||||
import { useTreeState } from '@/store/useTreeState'
|
||||
import { useAlertsStore } from '@/store/useAlertsStore'
|
||||
import { SshecretAdmin } from '@/client'
|
||||
import { SshecretObjectType } from '@/api/types'
|
||||
import SecretGroup from '@/components/secrets/SecretGroup.vue'
|
||||
import SecretGroupTreeItem from '@/components/secrets/SecretGroupTreeItem.vue'
|
||||
import SecretGroupTreeEntry from '@/components/secrets/SecretGroupTreeEntry.vue'
|
||||
import AddGroup from '@/components/secrets/AddGroup.vue'
|
||||
import SecretForm from '@/components/secrets/SecretForm.vue'
|
||||
import Drawer from '@/components/common/Drawer.vue'
|
||||
|
||||
const treeState = useTreeState()
|
||||
const alerts = useAlertsStore()
|
||||
|
||||
const ungrouped = computed(() => treeState.secretGroups.ungrouped)
|
||||
const secretGroups = computed(() => treeState.secretGroups.groups)
|
||||
|
||||
const groupSelected = computed(() => {
|
||||
if (!treeState.selected) {
|
||||
return false
|
||||
}
|
||||
if (treesState.selected.objectType === SshecretObject.SecretGroup) {
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
const drawerKey = ref(0)
|
||||
const createDrawerKey = ref(0)
|
||||
const createGroupDrawer = ref<boolean>(false)
|
||||
const createSecretDrawer = ref<boolean>(false)
|
||||
|
||||
function cancelCreateGroup() {
|
||||
createGroupDrawer.value = false
|
||||
drawerKey.value += 1
|
||||
}
|
||||
|
||||
const currentPath = computed(() => {
|
||||
if (treeState.selected && treeState.selected.objectType === SshecretObjectType.SecretGroup) {
|
||||
return treeState.selected.id
|
||||
}
|
||||
return ''
|
||||
})
|
||||
|
||||
const createSecretParent = computed(() => {
|
||||
if (treeState.selected && treeState.selected.objectType === SshecretObjectType.SecretGroup) {
|
||||
return treeState.selected.id
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
async function loadGroups() {
|
||||
await treeState.getSecretGroups()
|
||||
}
|
||||
|
||||
const drawerKeyName = computed(() => `${currentPath.value}_${drawerKey.value}`)
|
||||
|
||||
async function itemSelected(event: Event) {
|
||||
if (event.detail.selection.length == 0) {
|
||||
treeState.unselect()
|
||||
} else {
|
||||
const el = event.detail.selection[0] as HTMLElement
|
||||
const childType = el.dataset.type
|
||||
if (childType === 'secret') {
|
||||
const secretName = el.dataset.name
|
||||
treeState.selectSecret(secretName, null)
|
||||
} else if (childType === 'group') {
|
||||
const groupPath = el.dataset.groupPath
|
||||
if (groupPath === 'ungrouped') {
|
||||
treeState.unselect()
|
||||
} else {
|
||||
treeState.selectGroup(groupPath)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async function createGroup(path: string) {
|
||||
// Create a group
|
||||
console.log('Submit called')
|
||||
const response = await SshecretAdmin.addSecretGroupApiV1SecretsGroupsPost({
|
||||
body: {
|
||||
name: path,
|
||||
},
|
||||
})
|
||||
if (response.status === 200) {
|
||||
console.log('Success. Group created.')
|
||||
alerts.showAlert('Group created', 'success')
|
||||
createGroupDrawer.value = false
|
||||
drawerKey.value += 1
|
||||
|
||||
await loadGroups()
|
||||
} else {
|
||||
console.error(response)
|
||||
alerts.showAlert('Group creation failed', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
async function createSecret(secretCreate: SecretCreate) {
|
||||
console.log('Creating secret')
|
||||
const response = await SshecretAdmin.addSecretApiV1SecretsPost({
|
||||
body: secretCreate,
|
||||
})
|
||||
if (response.status == 200) {
|
||||
alerts.showAlert('Secret created', 'success')
|
||||
// We can close the drawer now.
|
||||
createSecretDrawer.value = false
|
||||
createDrawerKey.value += 1
|
||||
|
||||
await loadGroups()
|
||||
// Also update all the clients affected
|
||||
for (const clientId in secretCreate.clients) {
|
||||
console.log('Refreshing client: ', clientId)
|
||||
await treeState.refreshClient(clientId)
|
||||
}
|
||||
|
||||
treeState.selectSecret(secretCreate.name)
|
||||
} else {
|
||||
console.log(response)
|
||||
alerts.showAlert('Secret creation failed', 'error')
|
||||
}
|
||||
}
|
||||
|
||||
watch(
|
||||
() => treeState.secretGroupRevision,
|
||||
() => {
|
||||
treeState.getSecretGroups()
|
||||
},
|
||||
)
|
||||
|
||||
onMounted(loadGroups)
|
||||
</script>
|
||||
Reference in New Issue
Block a user