Implement routes and transitions
This commit is contained in:
23
packages/sshecret-frontend/src/api/paths.ts
Normal file
23
packages/sshecret-frontend/src/api/paths.ts
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
|
||||||
|
/*
|
||||||
|
* Split a path like /foo/bar/baz into ['foo', 'bar', 'baz']
|
||||||
|
*/
|
||||||
|
export function splitPath(path: string): string[] {
|
||||||
|
if (path.startsWith('/')) {
|
||||||
|
const newPath = path.substring(1)
|
||||||
|
return newPath.split("/")
|
||||||
|
}
|
||||||
|
return path.split("/")
|
||||||
|
}
|
||||||
|
|
||||||
|
/*
|
||||||
|
* Reassemble a path array like ['foo', 'bar', 'baz'] into /foo/bar/baz
|
||||||
|
*/
|
||||||
|
export function reassemblePath(segments: string[]): string {
|
||||||
|
const elements = segments.join('/')
|
||||||
|
return '/' + elements
|
||||||
|
}
|
||||||
|
|
||||||
|
export function idKey(id: string): string {
|
||||||
|
return 'id:' + id
|
||||||
|
}
|
||||||
@ -8,6 +8,7 @@ export enum SshecretObjectType {
|
|||||||
Client = "Client",
|
Client = "Client",
|
||||||
ClientSecret = "ClientSecret",
|
ClientSecret = "ClientSecret",
|
||||||
SecretGroup = "SecretGroup",
|
SecretGroup = "SecretGroup",
|
||||||
|
Audit = "Audit", // Not technically an object, but added for navigational purposes.
|
||||||
}
|
}
|
||||||
|
|
||||||
export type SshecretObject = {
|
export type SshecretObject = {
|
||||||
@ -71,3 +72,8 @@ export const SUBSYSTEM = [
|
|||||||
'sshd',
|
'sshd',
|
||||||
'backend',
|
'backend',
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
|
export interface PageState {
|
||||||
|
activePane: 'clients' | 'secrets' | 'audit',
|
||||||
|
selectedObject?: string,
|
||||||
|
}
|
||||||
|
|||||||
@ -0,0 +1,108 @@
|
|||||||
|
<template>
|
||||||
|
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-600">
|
||||||
|
<thead class="bg-gray-50 dark:bg-gray-700">
|
||||||
|
<tr>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
class="w-[1rem] p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white"
|
||||||
|
>
|
||||||
|
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white"
|
||||||
|
>
|
||||||
|
<sl-skeleton class="headerSkeleton"></sl-skeleton>
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white"
|
||||||
|
>
|
||||||
|
<sl-skeleton class="headerSkeleton"></sl-skeleton>
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white"
|
||||||
|
>
|
||||||
|
<sl-skeleton class="headerSkeleton"></sl-skeleton>
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white"
|
||||||
|
>
|
||||||
|
<sl-skeleton class="headerSkeleton"></sl-skeleton>
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white"
|
||||||
|
>
|
||||||
|
<sl-skeleton class="headerSkeleton"></sl-skeleton>
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white"
|
||||||
|
>
|
||||||
|
<sl-skeleton class="headerSkeleton"></sl-skeleton>
|
||||||
|
</th>
|
||||||
|
<th
|
||||||
|
scope="col"
|
||||||
|
class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white"
|
||||||
|
>
|
||||||
|
<sl-skeleton class="headerSkeleton"></sl-skeleton>
|
||||||
|
</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody class="bg-white dark:bg-gray-800">
|
||||||
|
<template v-for="row in 25">
|
||||||
|
<tr class="auditRow hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||||
|
<td class="w-[1rem]">
|
||||||
|
<sl-skeleton class="iconSkeleton"></sl-skeleton>
|
||||||
|
</td>
|
||||||
|
<template v-for="col in 7">
|
||||||
|
<td><sl-skeleton effect="pulse" class="tableSkeleton"></sl-skeleton></td>
|
||||||
|
</template>
|
||||||
|
</tr>
|
||||||
|
</template>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
<div
|
||||||
|
class="sticky bottom-0 right-0 items-center w-full p-4 bg-white border-t border-gray-200 sm:flex sm:justify-between dark:bg-gray-800 dark:border-gray-700"
|
||||||
|
>
|
||||||
|
<div class="flex items-center mb-4 sm:mb-0">
|
||||||
|
<span class="text-sm font-normal text-gray-500 dark:text-gray-400">
|
||||||
|
<sl-skeleton effect="pulse" style="width: 10rem"></sl-skeleton>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex items-center space-x-3">
|
||||||
|
<div class="flex space-x-1">
|
||||||
|
<sl-skeleton effect="pulse" style="width: 2rem"></sl-skeleton>
|
||||||
|
<sl-skeleton effect="pulse" style="width: 1rem"></sl-skeleton>
|
||||||
|
<sl-skeleton effect="pulse" style="width: 15rem"></sl-skeleton>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
tr.auditRow {
|
||||||
|
background-color: var(--color-white);
|
||||||
|
}
|
||||||
|
tr.auditRow:nth-child(even) {
|
||||||
|
background-color: var(--color-gray-50);
|
||||||
|
}
|
||||||
|
sl-skeleton.tableSkeleton {
|
||||||
|
height: 1rem;
|
||||||
|
margin: 1rem;
|
||||||
|
}
|
||||||
|
sl-skeleton.headerSkeleton {
|
||||||
|
height: 1rem;
|
||||||
|
margin: 1rem;
|
||||||
|
width: 50%;
|
||||||
|
}
|
||||||
|
|
||||||
|
sl-skeleton.iconSkeleton {
|
||||||
|
margin: 1em;
|
||||||
|
width: 1em;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,182 +1,190 @@
|
|||||||
<template>
|
<template>
|
||||||
<table v-if="auditEntries" class="min-w-full divide-y divide-gray-200 dark:divide-gray-600">
|
<template v-if="auditEntries">
|
||||||
<thead class="bg-gray-50 dark:bg-gray-700">
|
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-600">
|
||||||
<tr>
|
<thead class="bg-gray-50 dark:bg-gray-700">
|
||||||
<th
|
<tr>
|
||||||
scope="col"
|
<th
|
||||||
class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white"
|
scope="col"
|
||||||
>
|
class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white"
|
||||||
|
>
|
||||||
</th>
|
|
||||||
<th
|
</th>
|
||||||
scope="col"
|
<th
|
||||||
class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white"
|
scope="col"
|
||||||
>
|
class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white"
|
||||||
Timestamp
|
>
|
||||||
</th>
|
Timestamp
|
||||||
<th
|
</th>
|
||||||
scope="col"
|
<th
|
||||||
class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white"
|
scope="col"
|
||||||
>
|
class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white"
|
||||||
Subsystem
|
>
|
||||||
</th>
|
Subsystem
|
||||||
<th
|
</th>
|
||||||
scope="col"
|
<th
|
||||||
class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white"
|
scope="col"
|
||||||
>
|
class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white"
|
||||||
Operation
|
>
|
||||||
</th>
|
Operation
|
||||||
<th
|
</th>
|
||||||
scope="col"
|
<th
|
||||||
class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white"
|
scope="col"
|
||||||
>
|
class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white"
|
||||||
Client
|
>
|
||||||
</th>
|
Client
|
||||||
<th
|
</th>
|
||||||
scope="col"
|
<th
|
||||||
class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white"
|
scope="col"
|
||||||
>
|
class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white"
|
||||||
Secret
|
>
|
||||||
</th>
|
Secret
|
||||||
<th
|
</th>
|
||||||
scope="col"
|
<th
|
||||||
class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white"
|
scope="col"
|
||||||
>
|
class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white"
|
||||||
Message
|
>
|
||||||
</th>
|
Message
|
||||||
<th
|
</th>
|
||||||
scope="col"
|
<th
|
||||||
class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white"
|
scope="col"
|
||||||
>
|
class="p-4 text-xs font-medium tracking-wider text-left text-gray-500 uppercase dark:text-white"
|
||||||
Origin
|
>
|
||||||
</th>
|
Origin
|
||||||
</tr>
|
</th>
|
||||||
</thead>
|
|
||||||
<tbody class="bg-white dark:bg-gray-800">
|
|
||||||
<template v-for="entry in auditEntries" :key="entry.id">
|
|
||||||
<tr class="auditRow hover:bg-gray-100 dark:hover:bg-gray-700">
|
|
||||||
<td>
|
|
||||||
<sl-icon-button
|
|
||||||
name="chevron-right"
|
|
||||||
@click="toggle(entry.id)"
|
|
||||||
:class="{ 'rotate-90': isExpanded(entry.id) }"
|
|
||||||
></sl-icon-button>
|
|
||||||
</td>
|
|
||||||
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
|
|
||||||
{{ entry.timestamp }}
|
|
||||||
</td>
|
|
||||||
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
|
|
||||||
{{ entry.subsystem }}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
|
|
||||||
{{ entry.operation }}
|
|
||||||
</td>
|
|
||||||
|
|
||||||
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
|
|
||||||
<abbr :title="entry.client_id" v-if="entry.client_name">{{ entry.client_name }}</abbr>
|
|
||||||
</td>
|
|
||||||
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
|
|
||||||
<abbr :title="entry.secret_id" v-if="entry.secret_name">{{ entry.secret_name }}</abbr>
|
|
||||||
</td>
|
|
||||||
<td class="p-4 text-sm font-normal text-gray-500 whitespace-nowrap dark:text-gray-400">
|
|
||||||
{{ entry.message }}
|
|
||||||
</td>
|
|
||||||
<td class="p-4 text-sm font-normal text-gray-500 whitespace-nowrap dark:text-gray-400">
|
|
||||||
{{ entry.origin }}
|
|
||||||
</td>
|
|
||||||
</tr>
|
</tr>
|
||||||
<tr v-if="isExpanded(entry.id)" class="auditRow">
|
</thead>
|
||||||
<td></td>
|
<tbody class="bg-white dark:bg-gray-800">
|
||||||
<td colspan="8">
|
<template v-for="entry in auditEntries" :key="entry.id">
|
||||||
<dl
|
<tr class="auditRow hover:bg-gray-100 dark:hover:bg-gray-700">
|
||||||
class="max-w-md text-gray-900 divide-y divide-gray-200 dark:text-white dark:divide-gray-700 px-2 py-2"
|
<td>
|
||||||
>
|
<sl-icon-button
|
||||||
<div class="flex flex-col pb-3">
|
name="chevron-right"
|
||||||
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">ID</dt>
|
@click="toggle(entry.id)"
|
||||||
<dd class="text-xs font-semibold">{{ entry.id }}</dd>
|
:class="{ 'rotate-90': isExpanded(entry.id) }"
|
||||||
</div>
|
></sl-icon-button>
|
||||||
<div class="flex flex-col pb-3">
|
</td>
|
||||||
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Subsystem</dt>
|
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
|
||||||
<dd class="text-xs font-semibold">{{ entry.subsystem }}</dd>
|
{{ entry.timestamp }}
|
||||||
</div>
|
</td>
|
||||||
<div class="flex flex-col pb-3">
|
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
|
||||||
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Timestamp</dt>
|
{{ entry.subsystem }}
|
||||||
<dd class="text-xs font-semibold">{{ entry.timestamp }}</dd>
|
</td>
|
||||||
</div>
|
|
||||||
<div class="flex flex-col pb-3">
|
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
|
||||||
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Operation</dt>
|
{{ entry.operation }}
|
||||||
<dd class="text-xs font-semibold">{{ entry.operation }}</dd>
|
</td>
|
||||||
</div>
|
|
||||||
<div class="flex flex-col pb-3">
|
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
|
||||||
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Client ID</dt>
|
<abbr :title="entry.client_id" v-if="entry.client_name">{{ entry.client_name }}</abbr>
|
||||||
<dd class="text-xs font-semibold">{{ entry.client_id }}</dd>
|
</td>
|
||||||
</div>
|
<td class="p-4 text-sm font-normal text-gray-900 whitespace-nowrap dark:text-white">
|
||||||
<div class="flex flex-col pb-3">
|
<abbr :title="entry.secret_id" v-if="entry.secret_name">{{ entry.secret_name }}</abbr>
|
||||||
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Client Name</dt>
|
</td>
|
||||||
<dd class="text-xs font-semibold">{{ entry.client_name }}</dd>
|
<td class="p-4 text-sm font-normal text-gray-500 whitespace-nowrap dark:text-gray-400">
|
||||||
</div>
|
{{ entry.message }}
|
||||||
<div class="flex flex-col pb-3">
|
</td>
|
||||||
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Secret ID</dt>
|
<td class="p-4 text-sm font-normal text-gray-500 whitespace-nowrap dark:text-gray-400">
|
||||||
<dd class="text-xs font-semibold">{{ entry.secret_id }}</dd>
|
{{ entry.origin }}
|
||||||
</div>
|
</td>
|
||||||
<div class="flex flex-col pb-3">
|
</tr>
|
||||||
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Secret Name</dt>
|
<tr v-if="isExpanded(entry.id)" class="auditRow">
|
||||||
<dd class="text-xs font-semibold">{{ entry.secret_name }}</dd>
|
<td></td>
|
||||||
</div>
|
<td colspan="8">
|
||||||
<div class="flex flex-col pb-3">
|
<dl
|
||||||
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Message</dt>
|
class="max-w-md text-gray-900 divide-y divide-gray-200 dark:text-white dark:divide-gray-700 px-2 py-2"
|
||||||
<dd class="text-xs font-semibold">{{ entry.message }}</dd>
|
>
|
||||||
</div>
|
<div class="flex flex-col pb-3">
|
||||||
<div class="flex flex-col pb-3">
|
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">ID</dt>
|
||||||
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Origin</dt>
|
<dd class="text-xs font-semibold">{{ entry.id }}</dd>
|
||||||
<dd class="text-xs font-semibold">{{ entry.origin }}</dd>
|
</div>
|
||||||
</div>
|
<div class="flex flex-col pb-3">
|
||||||
<template v-if="entry.data">
|
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Subsystem</dt>
|
||||||
<template v-for="(value, key) in entry.data">
|
<dd class="text-xs font-semibold">{{ entry.subsystem }}</dd>
|
||||||
<div class="flex flex-col pb-3">
|
</div>
|
||||||
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">{{ key }}</dt>
|
<div class="flex flex-col pb-3">
|
||||||
<dd class="text-xs font-semibold">{{ value }}</dd>
|
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Timestamp</dt>
|
||||||
</div>
|
<dd class="text-xs font-semibold">{{ entry.timestamp }}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col pb-3">
|
||||||
|
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Operation</dt>
|
||||||
|
<dd class="text-xs font-semibold">{{ entry.operation }}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col pb-3">
|
||||||
|
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Client ID</dt>
|
||||||
|
<dd class="text-xs font-semibold">{{ entry.client_id }}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col pb-3">
|
||||||
|
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Client Name</dt>
|
||||||
|
<dd class="text-xs font-semibold">{{ entry.client_name }}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col pb-3">
|
||||||
|
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Secret ID</dt>
|
||||||
|
<dd class="text-xs font-semibold">{{ entry.secret_id }}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col pb-3">
|
||||||
|
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Secret Name</dt>
|
||||||
|
<dd class="text-xs font-semibold">{{ entry.secret_name }}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col pb-3">
|
||||||
|
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Message</dt>
|
||||||
|
<dd class="text-xs font-semibold">{{ entry.message }}</dd>
|
||||||
|
</div>
|
||||||
|
<div class="flex flex-col pb-3">
|
||||||
|
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">Origin</dt>
|
||||||
|
<dd class="text-xs font-semibold">{{ entry.origin }}</dd>
|
||||||
|
</div>
|
||||||
|
<template v-if="entry.data">
|
||||||
|
<template v-for="(value, key) in entry.data">
|
||||||
|
<div class="flex flex-col pb-3">
|
||||||
|
<dt class="mb-1 text-gray-500 md:text-xs dark:text-gray-400">{{ key }}</dt>
|
||||||
|
<dd class="text-xs font-semibold">{{ value }}</dd>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
</template>
|
</dl>
|
||||||
</dl>
|
</td>
|
||||||
</td>
|
</tr>
|
||||||
</tr>
|
</template>
|
||||||
</template>
|
</tbody>
|
||||||
</tbody>
|
</table>
|
||||||
</table>
|
<div
|
||||||
<div
|
class="sticky bottom-0 right-0 items-center w-full p-4 bg-white border-t border-gray-200 sm:flex sm:justify-between dark:bg-gray-800 dark:border-gray-700"
|
||||||
class="sticky bottom-0 right-0 items-center w-full p-4 bg-white border-t border-gray-200 sm:flex sm:justify-between dark:bg-gray-800 dark:border-gray-700"
|
v-if="totalPages > 0"
|
||||||
v-if="totalPages > 0"
|
>
|
||||||
>
|
<div class="flex items-center mb-4 sm:mb-0">
|
||||||
<div class="flex items-center mb-4 sm:mb-0">
|
<span class="text-sm font-normal text-gray-500 dark:text-gray-400">
|
||||||
<span class="text-sm font-normal text-gray-500 dark:text-gray-400">
|
Showing
|
||||||
Showing
|
<span
|
||||||
<span class="font-semibold text-gray-900 dark:text-white" v-if="totalEntries < lastResult">
|
class="font-semibold text-gray-900 dark:text-white"
|
||||||
{{ firstResult }}-{{ TotalEntries }}
|
v-if="totalEntries < lastResult"
|
||||||
|
>
|
||||||
|
{{ firstResult }}-{{ TotalEntries }}
|
||||||
|
</span>
|
||||||
|
<span class="font-semibold text-gray-900 dark:text-white" v-else>
|
||||||
|
{{ firstResult }}-{{ lastResult }}
|
||||||
|
</span>
|
||||||
|
of
|
||||||
|
<span class="font-semibold text-gray-900 dark:text-white">
|
||||||
|
{{ totalEntries }}
|
||||||
|
</span>
|
||||||
</span>
|
</span>
|
||||||
<span class="font-semibold text-gray-900 dark:text-white" v-else>
|
</div>
|
||||||
{{ firstResult }}-{{ lastResult }}
|
<div class="flex items-center space-x-3">
|
||||||
</span>
|
<div class="flex space-x-1">
|
||||||
of
|
<PageNumbers
|
||||||
<span class="font-semibold text-gray-900 dark:text-white">
|
@next="nextPage"
|
||||||
{{ totalEntries }}
|
@previous="prevPage"
|
||||||
</span>
|
@goto="goToPage"
|
||||||
</span>
|
:pageNum="pageNum"
|
||||||
</div>
|
:totalPages="totalPages"
|
||||||
<div class="flex items-center space-x-3">
|
/>
|
||||||
<div class="flex space-x-1">
|
</div>
|
||||||
<PageNumbers
|
|
||||||
@next="nextPage"
|
|
||||||
@previous="prevPage"
|
|
||||||
@goto="goToPage"
|
|
||||||
:pageNum="pageNum"
|
|
||||||
:totalPages="totalPages"
|
|
||||||
/>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<AuditSkeleton />
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, reactive, onMounted, watch, toRef } from 'vue'
|
import { computed, ref, reactive, onMounted, watch, toRef } from 'vue'
|
||||||
@ -186,6 +194,7 @@ import { SshecretAdmin, GetAuditLogApiV1AuditGetData } from '@/client'
|
|||||||
import type { AuditListResult } from '@/client'
|
import type { AuditListResult } from '@/client'
|
||||||
import type { AuditFilter } from '@/api/types'
|
import type { AuditFilter } from '@/api/types'
|
||||||
import PageNumbers from '@/components/common/PageNumbers.vue'
|
import PageNumbers from '@/components/common/PageNumbers.vue'
|
||||||
|
import AuditSkeleton from '@/components/audit/AuditSkeleton.vue'
|
||||||
|
|
||||||
const props = defineProps<{ auditFilter: AuditFilter }>()
|
const props = defineProps<{ auditFilter: AuditFilter }>()
|
||||||
|
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
<template>
|
<template>
|
||||||
<sl-tree-item :id="itemId" data-type="secret" :data-name="name" :data-parent-id="parent_id">
|
<sl-tree-item :id="itemId" data-type="secret" :data-name="name" :data-parent-id="props.parentId">
|
||||||
<sl-icon name="file-lock2"> </sl-icon>
|
<sl-icon name="file-lock2"> </sl-icon>
|
||||||
<span class="px-2">{{ name }}</span>
|
<span class="px-2">{{ name }}</span>
|
||||||
</sl-tree-item>
|
</sl-tree-item>
|
||||||
@ -12,5 +12,5 @@ const props = defineProps<{
|
|||||||
name: string
|
name: string
|
||||||
selected?: boolean
|
selected?: boolean
|
||||||
}>()
|
}>()
|
||||||
const itemId = computed(() => `client-${props.parent_id}-secret-${props.name}`)
|
const itemId = computed(() => `client-${props.parentId}-secret-${props.name}`)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -69,9 +69,3 @@ p sl-skeleton {
|
|||||||
width: 10rem;
|
width: 10rem;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
<script setup lang="ts">
|
|
||||||
import '@shoelace-style/shoelace/dist/components/tab-group/tab-group.js'
|
|
||||||
import '@shoelace-style/shoelace/dist/components/tab/tab.js'
|
|
||||||
import '@shoelace-style/shoelace/dist/components/skeleton/skeleton.js'
|
|
||||||
</script>
|
|
||||||
|
|||||||
@ -0,0 +1,15 @@
|
|||||||
|
<template></template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
/*
|
||||||
|
This component is responsible for loading the correct object based on props and state.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import AuditView from '@/views/audit/AuditView.vue'
|
||||||
|
import ClientDetailView from '@/views/clients/ClientDetailView.vue'
|
||||||
|
import SecretDetailView from '@/views/secrets/SecretDetailView.vue'
|
||||||
|
import SecretGroupDetailView from '@/views/secrets/SecretGroupDetailView.vue'
|
||||||
|
import { SshecretObjectType } from '@/api/types'
|
||||||
|
import { useAuditFilterState } from '@/store/useAuditFilterState'
|
||||||
|
import { useTreeState } from '@/store/useTreeState'
|
||||||
|
import { useAlertsStore } from '@/store/useAlertsStore'
|
||||||
|
</script>
|
||||||
@ -0,0 +1,3 @@
|
|||||||
|
<template>
|
||||||
|
<p class="p-4 text-gray-500 dark:text-gray-200">Select an item to view details</p>
|
||||||
|
</template>
|
||||||
@ -0,0 +1,109 @@
|
|||||||
|
<template>
|
||||||
|
<MasterDetail>
|
||||||
|
<template #master>
|
||||||
|
<MasterTabs :selectedTab="selectedTab" @change="tabSelected" />
|
||||||
|
</template>
|
||||||
|
<template #detail v-if="showAudit || selectedTab === 'audit'">
|
||||||
|
<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 type { PageState } from '@/api/types'
|
||||||
|
import { computed, ref, onMounted } from 'vue'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import MasterTabs from '@/components/common/MasterTabs.vue'
|
||||||
|
import MasterDetail from '@/views/layout/MasterDetail.vue'
|
||||||
|
import AuditView from '@/views/audit/AuditView.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 GenericDetail from '@/components/common/GenericDetail.vue'
|
||||||
|
|
||||||
|
import { SshecretObjectType } from '@/api/types'
|
||||||
|
import { useAuditFilterState } from '@/store/useAuditFilterState'
|
||||||
|
import { useTreeState } from '@/store/useTreeState'
|
||||||
|
import { useAlertsStore } from '@/store/useAlertsStore'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const alerts = useAlertsStore()
|
||||||
|
|
||||||
|
const treeState = useTreeState()
|
||||||
|
|
||||||
|
const auditFilterState = useAuditFilterState()
|
||||||
|
|
||||||
|
const showAudit = ref<{ boolean }>()
|
||||||
|
|
||||||
|
const props = defineProps<{ loadPage: PageState }>()
|
||||||
|
|
||||||
|
const selectedTab = computed(() => props.loadPage?.activePane)
|
||||||
|
const selectedClientName = ref()
|
||||||
|
|
||||||
|
async function loadObjectSelection() {
|
||||||
|
if (!props.loadPage) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if (props.loadPage.activePane === 'audit') {
|
||||||
|
treeState.showAudit = true
|
||||||
|
showAudit.value = true
|
||||||
|
}
|
||||||
|
if (props.loadPage.activePane === 'clients' && props.loadPage.selectedObject) {
|
||||||
|
try {
|
||||||
|
await treeState.loadClientName(props.loadPage.selectedObject)
|
||||||
|
selectedClientName.value = props.loadPage.selectedObject
|
||||||
|
} catch (e) {
|
||||||
|
// We need to figure out how to generate a 404 here
|
||||||
|
alerts.showAlert('Could not find the object', 'error')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function tabSelected(tabName) {
|
||||||
|
router.push({ name: tabName })
|
||||||
|
if (tabName == 'audit') {
|
||||||
|
treeState.showAudit = true
|
||||||
|
showAudit.value = true
|
||||||
|
} else {
|
||||||
|
treeState.showAudit = false
|
||||||
|
showAudit.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(loadObjectSelection)
|
||||||
|
</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>
|
||||||
@ -0,0 +1,36 @@
|
|||||||
|
<template>
|
||||||
|
<sl-tab-group
|
||||||
|
id="sideTabs"
|
||||||
|
class="flex flex-col flex-1 h-full overflow-hidden master-pane-tabs"
|
||||||
|
@sl-tab-show="emit('change', $event.detail.name)"
|
||||||
|
>
|
||||||
|
<sl-tab slot="nav" panel="clients" :active="selectedTab === 'clients'">Clients</sl-tab>
|
||||||
|
<sl-tab slot="nav" panel="secrets" :active="selectedTab === 'secrets'">Secrets</sl-tab>
|
||||||
|
<sl-tab slot="nav" panel="audit" :active="selectedTab === 'audit'">Audit</sl-tab>
|
||||||
|
<sl-tab-panel name="clients">
|
||||||
|
<slot name="clients">
|
||||||
|
<ClientTreeList />
|
||||||
|
</slot>
|
||||||
|
</sl-tab-panel>
|
||||||
|
<sl-tab-panel name="secrets">
|
||||||
|
<slot name="secrets">
|
||||||
|
<SecretTreeList />
|
||||||
|
</slot>
|
||||||
|
</sl-tab-panel>
|
||||||
|
<sl-tab-panel name="audit">
|
||||||
|
<slot name="audit">
|
||||||
|
<AuditFilters />
|
||||||
|
</slot>
|
||||||
|
</sl-tab-panel>
|
||||||
|
</sl-tab-group>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { toRef } from 'vue'
|
||||||
|
import ClientTreeList from '@/views/clients/ClientTreeList.vue'
|
||||||
|
import SecretTreeList from '@/views/secrets/SecretTreeList.vue'
|
||||||
|
import AuditFilters from '@/components/audit/AuditFilters.vue'
|
||||||
|
|
||||||
|
const props = defineProps<{ selectedTab: string }>()
|
||||||
|
const selectedTab = toRef(() => props.selectedTab)
|
||||||
|
const emit = defineEmits<{ (e: 'change', data: string): void }>()
|
||||||
|
</script>
|
||||||
@ -0,0 +1,15 @@
|
|||||||
|
<template>
|
||||||
|
<sl-tree-item>
|
||||||
|
<sl-skeleton effect="sheen" class="tree-icon-skeleton"></sl-skeleton>
|
||||||
|
<sl-skeleton effect="sheen" class="tree-item-skeleton"></sl-skeleton>
|
||||||
|
</sl-tree-item>
|
||||||
|
</template>
|
||||||
|
<style>
|
||||||
|
sl-skeleton.tree-icon-skeleton {
|
||||||
|
width: 1rem;
|
||||||
|
margin-right: 1rem;
|
||||||
|
}
|
||||||
|
sl-skeleton.tree-item-skeleton {
|
||||||
|
width: 6rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@ -1,43 +1,71 @@
|
|||||||
<template>
|
<template>
|
||||||
<div
|
<sl-tab-group>
|
||||||
role="status"
|
<sl-tab slot="nav" panel="skeleton"><sl-skeleton></sl-skeleton></sl-tab>
|
||||||
class="w-full p-4 space-y-4 border border-gray-200 divide-y divide-gray-200 rounded-sm shadow-sm animate-pulse dark:divide-gray-700 md:p-6 dark:border-gray-700"
|
|
||||||
>
|
<sl-tab-panel name="skeleton">
|
||||||
<div class="flex items-center justify-between">
|
<div id="client_details">
|
||||||
<div>
|
<div class="w-full p-2">
|
||||||
<div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
|
<div class="px-4 sm:px-0">
|
||||||
<div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
|
<h3 class="text-base/7 font-semibold text-gray-900 dark:text-gray-50">
|
||||||
|
<sl-skeleton effect="pulse"></sl-skeleton>
|
||||||
|
</h3>
|
||||||
|
<p class="mt-1 max-w-2xl text-sm/6 text-gray-500 dark:text-gray-100">
|
||||||
|
<sl-skeleton effect="pulse"></sl-skeleton>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div class="mt-6 border-t border-gray-100">
|
||||||
|
<dl class="divide-y divide-gray-100">
|
||||||
|
<div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||||
|
<dt class="text-sm/6 font-medium text-gray-900 dark:text-gray-200">
|
||||||
|
<sl-skeleton effect="pulse"></sl-skeleton>
|
||||||
|
</dt>
|
||||||
|
<dd class="mt-1 text-sm/6 text-gray-700 sm:col-span-2 sm:mt-0 dark:text-gray-300">
|
||||||
|
<sl-skeleton effect="pulse"></sl-skeleton>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
<div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||||
|
<dt class="text-sm/6 font-medium text-gray-900 dark:text-gray-200">
|
||||||
|
<sl-skeleton effect="pulse"></sl-skeleton>
|
||||||
|
</dt>
|
||||||
|
<dd class="mt-1 text-sm/6 text-gray-700 sm:col-span-2 sm:mt-0 dark:text-gray-300">
|
||||||
|
<sl-skeleton effect="pulse"></sl-skeleton>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||||
|
<dt class="text-sm/6 font-medium text-gray-900 dark:text-gray-200">
|
||||||
|
<sl-skeleton effect="pulse"></sl-skeleton>
|
||||||
|
</dt>
|
||||||
|
<dd class="mt-1 text-sm/6 text-gray-700 sm:col-span-2 sm:mt-0 dark:text-gray-300">
|
||||||
|
<sl-skeleton effect="pulse"></sl-skeleton>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="px-4 py-6 sm:grid sm:grid-cols-3 sm:gap-4 sm:px-0">
|
||||||
|
<dt class="text-sm/6 font-medium text-gray-900 dark:text-gray-200">
|
||||||
|
<sl-skeleton effect="pulse"></sl-skeleton>
|
||||||
|
</dt>
|
||||||
|
<dd class="mt-1 text-sm/6 text-gray-700 sm:col-span-2 sm:mt-0 dark:text-gray-300">
|
||||||
|
<sl-skeleton effect="pulse"></sl-skeleton>
|
||||||
|
</dd>
|
||||||
|
</div>
|
||||||
|
</dl>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
|
</sl-tab-panel>
|
||||||
</div>
|
</sl-tab-group>
|
||||||
<div class="flex items-center justify-between pt-4">
|
|
||||||
<div>
|
|
||||||
<div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
|
|
||||||
<div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
|
|
||||||
</div>
|
|
||||||
<div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-between pt-4">
|
|
||||||
<div>
|
|
||||||
<div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
|
|
||||||
<div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
|
|
||||||
</div>
|
|
||||||
<div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-between pt-4">
|
|
||||||
<div>
|
|
||||||
<div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
|
|
||||||
<div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
|
|
||||||
</div>
|
|
||||||
<div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
|
|
||||||
</div>
|
|
||||||
<div class="flex items-center justify-between pt-4">
|
|
||||||
<div>
|
|
||||||
<div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-600 w-24 mb-2.5"></div>
|
|
||||||
<div class="w-32 h-2 bg-gray-200 rounded-full dark:bg-gray-700"></div>
|
|
||||||
</div>
|
|
||||||
<div class="h-2.5 bg-gray-300 rounded-full dark:bg-gray-700 w-12"></div>
|
|
||||||
</div>
|
|
||||||
<span class="sr-only">Loading...</span>
|
|
||||||
</div>
|
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
sl-tab sl-skeleton {
|
||||||
|
width: 30%;
|
||||||
|
}
|
||||||
|
h3 sl-skeleton {
|
||||||
|
width: 15rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
p sl-skeleton {
|
||||||
|
width: 10rem;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|||||||
@ -30,6 +30,8 @@ import '@shoelace-style/shoelace/dist/components/option/option.js'
|
|||||||
import '@shoelace-style/shoelace/dist/components/range/range.js'
|
import '@shoelace-style/shoelace/dist/components/range/range.js'
|
||||||
import '@shoelace-style/shoelace/dist/components/select/select.js'
|
import '@shoelace-style/shoelace/dist/components/select/select.js'
|
||||||
import '@shoelace-style/shoelace/dist/components/skeleton/skeleton.js'
|
import '@shoelace-style/shoelace/dist/components/skeleton/skeleton.js'
|
||||||
|
import '@shoelace-style/shoelace/dist/components/spinner/spinner.js'
|
||||||
|
|
||||||
import '@shoelace-style/shoelace/dist/components/tab-group/tab-group.js'
|
import '@shoelace-style/shoelace/dist/components/tab-group/tab-group.js'
|
||||||
import '@shoelace-style/shoelace/dist/components/tab-panel/tab-panel.js'
|
import '@shoelace-style/shoelace/dist/components/tab-panel/tab-panel.js'
|
||||||
import '@shoelace-style/shoelace/dist/components/tab/tab.js'
|
import '@shoelace-style/shoelace/dist/components/tab/tab.js'
|
||||||
|
|||||||
@ -2,8 +2,15 @@ import { createRouter, createWebHistory } from 'vue-router'
|
|||||||
|
|
||||||
import LoginPage from '@/views/LoginPage.vue'
|
import LoginPage from '@/views/LoginPage.vue'
|
||||||
import WorkspaceView from '@/views/WorkspaceView.vue'
|
import WorkspaceView from '@/views/WorkspaceView.vue'
|
||||||
import AuditView from '@/views/audit/AuditView.vue'
|
import AuditPage from '@/views/audit/AuditPage.vue'
|
||||||
|
import ClientPage from '@/views/clients/ClientPage.vue'
|
||||||
|
import SecretPage from '@/views/secrets/SecretPage.vue'
|
||||||
|
import ClientDetailPage from '@/views/clients/ClientDetailPage.vue'
|
||||||
|
import SecretDetailView from '@/views/secrets/SecretDetailView.vue'
|
||||||
|
import SecretGroupDetailView from '@/views/secrets/SecretGroupDetailView.vue'
|
||||||
|
import GenericDetail from '@/components/common/GenericDetail.vue'
|
||||||
import { useAuthTokenStore } from '@/store/auth'
|
import { useAuthTokenStore } from '@/store/auth'
|
||||||
|
import { reassemblePath } from '@/api/paths'
|
||||||
|
|
||||||
|
|
||||||
const routes = [
|
const routes = [
|
||||||
@ -15,7 +22,55 @@ const routes = [
|
|||||||
meta: { requiresAuth: true },
|
meta: { requiresAuth: true },
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: '/audit', name: 'audit', component: AuditView, meta: { requiresAuth: true },
|
path: '/audit', name: 'audit', component: AuditPage, meta: { requiresAuth: true }
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/clients', component: ClientPage, meta: { requiresAuth: true }, children: [
|
||||||
|
{
|
||||||
|
path: '', component: GenericDetail, name: 'clients', meta: { requiresAuth: true },
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Client',
|
||||||
|
path: ':id',
|
||||||
|
component: ClientDetailPage,
|
||||||
|
props: true,
|
||||||
|
meta: { requiresAuth: true },
|
||||||
|
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'ClientSecret',
|
||||||
|
path: ':parentId/:id',
|
||||||
|
component: SecretDetailView,
|
||||||
|
props: true,
|
||||||
|
meta: { requiresAuth: true },
|
||||||
|
},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: '/secrets', component: SecretPage, meta: { requiresAuth: true }, children: [
|
||||||
|
{
|
||||||
|
path: '', component: GenericDetail, name: 'secrets',
|
||||||
|
meta: { requiresAuth: true },
|
||||||
|
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Secret',
|
||||||
|
path: ':id',
|
||||||
|
component: SecretDetailView,
|
||||||
|
props: true,
|
||||||
|
meta: { requiresAuth: true },
|
||||||
|
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: 'Group',
|
||||||
|
path: '/group/:groupPath(.*)*',
|
||||||
|
component: SecretGroupDetailView,
|
||||||
|
props: route => ({
|
||||||
|
groupPath: Array.isArray(route.params.groupPath) ? reassemblePath(route.params.groupPath) : route.params.groupPath || ''
|
||||||
|
}),
|
||||||
|
meta: { requiresAuth: true },
|
||||||
|
}
|
||||||
|
],
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@ -20,6 +20,22 @@ export const useTreeState = defineStore('treeState', {
|
|||||||
selectClient(id: string) {
|
selectClient(id: string) {
|
||||||
this.selected = { objectType: SshecretObjectType.Client, id: id }
|
this.selected = { objectType: SshecretObjectType.Client, id: id }
|
||||||
},
|
},
|
||||||
|
/*
|
||||||
|
* Fetch and select a specific client.
|
||||||
|
*/
|
||||||
|
async loadClientName(name: string) {
|
||||||
|
const existing = this.clients?.clients.find(c => c.name === name)
|
||||||
|
if (existing) {
|
||||||
|
this.selected = { objectType: SshecretObjectType.Client, id: existing.id, param: name }
|
||||||
|
} else {
|
||||||
|
const result = await this.queryClients(name, 0, 1)
|
||||||
|
if (result === 1 && this.clients) {
|
||||||
|
this.selected = { objectType: SshecretObjectType.Client, id: this.clients.clients[0].id, param: name }
|
||||||
|
} else {
|
||||||
|
throw new Error("The selected client could not be found.")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
selectSecret(name: string, parent?: string) {
|
selectSecret(name: string, parent?: string) {
|
||||||
this.selected = { objectType: SshecretObjectType.ClientSecret, id: name }
|
this.selected = { objectType: SshecretObjectType.ClientSecret, id: name }
|
||||||
if (parent) {
|
if (parent) {
|
||||||
|
|||||||
@ -1,93 +1,6 @@
|
|||||||
<template>
|
<template>
|
||||||
<MasterDetail>
|
<MasterDetailWorkspace />
|
||||||
<template #master>
|
|
||||||
<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="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>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref } from 'vue'
|
import MasterDetailWorkspace from '@/components/common/MasterDetailWorkspace.vue'
|
||||||
import AuditView from '@/views/audit/AuditView.vue'
|
|
||||||
import MasterDetail from '@/views/layout/MasterDetail.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>
|
</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>
|
|
||||||
|
|||||||
29
packages/sshecret-frontend/src/views/audit/AuditPage.vue
Normal file
29
packages/sshecret-frontend/src/views/audit/AuditPage.vue
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<template>
|
||||||
|
<MasterDetail>
|
||||||
|
<template #master>
|
||||||
|
<MasterTabs selectedTab="audit" @change="tabSelected" />
|
||||||
|
</template>
|
||||||
|
<template #detail>
|
||||||
|
<AuditView />
|
||||||
|
</template>
|
||||||
|
</MasterDetail>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import AuditView from '@/views/audit/AuditView.vue'
|
||||||
|
import MasterDetail from '@/views/layout/MasterDetail.vue'
|
||||||
|
import MasterTabs from '@/components/common/MasterTabs.vue'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { RouterView } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
function tabSelected(tabName: string) {
|
||||||
|
if (tabName !== 'audit') {
|
||||||
|
router.push({ name: tabName })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const routeKey = computed(() => route.name + '-' + (route.params.id ?? 'root'))
|
||||||
|
</script>
|
||||||
@ -2,10 +2,14 @@
|
|||||||
<template v-if="loaded">
|
<template v-if="loaded">
|
||||||
<AuditTable :auditFilter="auditFilter" />
|
<AuditTable :auditFilter="auditFilter" />
|
||||||
</template>
|
</template>
|
||||||
|
<template v-else>
|
||||||
|
<AuditSkeleton />
|
||||||
|
</template>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch, onMounted } from 'vue'
|
import { ref, watch, onMounted } from 'vue'
|
||||||
import AuditTable from '@/components/audit/AuditTable.vue'
|
import AuditTable from '@/components/audit/AuditTable.vue'
|
||||||
|
import AuditSkeleton from '@/components/audit/AuditSkeleton.vue'
|
||||||
import type { AuditFilter } from '@/api/types'
|
import type { AuditFilter } from '@/api/types'
|
||||||
import type { GetAuditLogApiV1AuditGetData } from '@/client'
|
import type { GetAuditLogApiV1AuditGetData } from '@/client'
|
||||||
import { useAuditFilterState } from '@/store/useAuditFilterState'
|
import { useAuditFilterState } from '@/store/useAuditFilterState'
|
||||||
@ -13,10 +17,10 @@ const auditFilterState = useAuditFilterState()
|
|||||||
const auditFilter = ref<GetAuditLogApiV1AuditGetData['query']>({})
|
const auditFilter = ref<GetAuditLogApiV1AuditGetData['query']>({})
|
||||||
|
|
||||||
watch(auditFilterState, () => (auditFilter.value = auditFilterState.getFilter))
|
watch(auditFilterState, () => (auditFilter.value = auditFilterState.getFilter))
|
||||||
const loaded = ref<{ boolean }>()
|
const loaded = ref<{ boolean }>(false)
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
loaded.value = true
|
|
||||||
auditFilter.value = auditFilterState.getFilter
|
auditFilter.value = auditFilterState.getFilter
|
||||||
|
loaded.value = true
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -0,0 +1,24 @@
|
|||||||
|
<template>
|
||||||
|
<ClientDetailView :id="clientId" :parentId="parentId" @clientDeleted="onClientDelete" />
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { toRef } from 'vue'
|
||||||
|
import ClientDetailView from '@/views/clients/ClientDetailView.vue'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { useTreeState } from '@/store/useTreeState'
|
||||||
|
|
||||||
|
const props = defineProps<{ id: string | null; parentId: string | null }>()
|
||||||
|
const router = useRouter()
|
||||||
|
const clientId = toRef(() => props.id)
|
||||||
|
const parentId = toRef(() => props.parentId)
|
||||||
|
|
||||||
|
const treeState = useTreeState()
|
||||||
|
|
||||||
|
async function onClientDelete(id: string) {
|
||||||
|
// React when a client is deleted
|
||||||
|
|
||||||
|
console.log('Client deleted')
|
||||||
|
await treeState.loadClients()
|
||||||
|
router.push({ name: 'clients' })
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@ -1,44 +1,54 @@
|
|||||||
<template>
|
<template>
|
||||||
<ClientDetail :client="client" @update="updateClient" @deleted="deleteClient" v-if="client" />
|
<ClientDetail
|
||||||
|
:client="client"
|
||||||
|
@update="updateClient"
|
||||||
|
@deleted="deleteClient"
|
||||||
|
v-if="client"
|
||||||
|
:key="clientId"
|
||||||
|
/>
|
||||||
<ClientSkeleton v-else />
|
<ClientSkeleton v-else />
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch, onMounted } from 'vue'
|
import { ref, toRef, watch, onMounted } from 'vue'
|
||||||
import ClientSkeleton from '@/components/clients/ClientSkeleton.vue'
|
import ClientSkeleton from '@/components/clients/ClientSkeleton.vue'
|
||||||
import ClientDetail from '@/components/clients/ClientDetail.vue'
|
import ClientDetail from '@/components/clients/ClientDetail.vue'
|
||||||
import type { ClientCreate } from '@/client'
|
import type { ClientCreate } from '@/client'
|
||||||
|
import { idKey } from '@/api/paths'
|
||||||
import { SshecretAdmin } from '@/client'
|
import { SshecretAdmin } from '@/client'
|
||||||
import { useTreeState } from '@/store/useTreeState'
|
import { useTreeState } from '@/store/useTreeState'
|
||||||
|
|
||||||
const props = defineProps<{ clientId: string | null }>()
|
const props = defineProps<{ id: string | null; parentId: string | null }>()
|
||||||
|
|
||||||
|
const clientId = toRef(() => props.id)
|
||||||
|
|
||||||
const client = ref<Client>()
|
const client = ref<Client>()
|
||||||
|
|
||||||
const treeState = useTreeState()
|
const treeState = useTreeState()
|
||||||
|
|
||||||
|
const emit = defineEmits<{ (e: 'clientDeleted', data: string): void }>()
|
||||||
|
|
||||||
async function loadClient() {
|
async function loadClient() {
|
||||||
if (!props.clientId) return
|
console.log('loadClient called: ', props.id)
|
||||||
client.value = await treeState.getClient()
|
if (!props.id) return
|
||||||
|
client.value = await treeState.getClient(props.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function deleteClient(clientId: string) {
|
async function deleteClient(deleteId: string) {
|
||||||
console.log(`Delete ${localClient.value.id}`)
|
|
||||||
const response = await SshecretAdmin.deleteClientApiV1ClientsIdDelete({
|
const response = await SshecretAdmin.deleteClientApiV1ClientsIdDelete({
|
||||||
path: { id: clientId },
|
path: { id: idKey(deleteId) },
|
||||||
})
|
})
|
||||||
if (response.status !== 200) {
|
if (response.status !== 200) {
|
||||||
console.error(response)
|
console.error(response)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
emit('clientDeleted', clientId)
|
emit('clientDeleted', deleteId)
|
||||||
props.clientId = null
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateClient(updated: ClientCreate) {
|
async function updateClient(updated: ClientCreate) {
|
||||||
const response = await SshecretAdmin.updateClientApiV1ClientsIdPut({
|
const response = await SshecretAdmin.updateClientApiV1ClientsIdPut({
|
||||||
path: { id: localClient.value.id },
|
path: { id: idKey(localClient.value.id) },
|
||||||
body: data,
|
body: data,
|
||||||
})
|
})
|
||||||
client.value = response.data
|
client.value = response.data
|
||||||
@ -47,7 +57,7 @@ async function updateClient(updated: ClientCreate) {
|
|||||||
onMounted(loadClient)
|
onMounted(loadClient)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.client_id,
|
() => props.id,
|
||||||
() => loadClient(),
|
() => loadClient(),
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|||||||
29
packages/sshecret-frontend/src/views/clients/ClientPage.vue
Normal file
29
packages/sshecret-frontend/src/views/clients/ClientPage.vue
Normal file
@ -0,0 +1,29 @@
|
|||||||
|
<template>
|
||||||
|
<MasterDetail>
|
||||||
|
<template #master>
|
||||||
|
<MasterTabs selectedTab="clients" @change="tabSelected" />
|
||||||
|
</template>
|
||||||
|
<template #detail>
|
||||||
|
<RouterView :key="routeKey" />
|
||||||
|
</template>
|
||||||
|
</MasterDetail>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import MasterDetail from '@/views/layout/MasterDetail.vue'
|
||||||
|
import MasterTabs from '@/components/common/MasterTabs.vue'
|
||||||
|
import ClientDetailView from '@/views/clients/ClientDetailView.vue'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { RouterView } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
function tabSelected(tabName: string) {
|
||||||
|
if (tabName !== 'clients') {
|
||||||
|
router.push({ name: tabName })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const routeKey = computed(() => route.name + '-' + (route.params.id ?? 'root'))
|
||||||
|
</script>
|
||||||
@ -44,6 +44,11 @@
|
|||||||
</ClientTreeItem>
|
</ClientTreeItem>
|
||||||
</template>
|
</template>
|
||||||
</sl-tree>
|
</sl-tree>
|
||||||
|
<sl-tree class="w-full" v-else>
|
||||||
|
<template v-for="n in 20">
|
||||||
|
<TreeItemSkeleton />
|
||||||
|
</template>
|
||||||
|
</sl-tree>
|
||||||
</div>
|
</div>
|
||||||
<div
|
<div
|
||||||
class="shrink-0 mt-4 pt-2 border-t border-gray-100 dark:border-gray-700 bg-white dark:bg-gray-800"
|
class="shrink-0 mt-4 pt-2 border-t border-gray-100 dark:border-gray-700 bg-white dark:bg-gray-800"
|
||||||
@ -88,7 +93,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, reactive, onMounted, watch } from 'vue'
|
import { computed, ref, reactive, toRef, onMounted, watch, nextTick } from 'vue'
|
||||||
import type { Ref } from 'vue'
|
import type { Ref } from 'vue'
|
||||||
|
|
||||||
import { usePagination } from '@/composables/usePagination'
|
import { usePagination } from '@/composables/usePagination'
|
||||||
@ -98,11 +103,13 @@ import { SshecretAdmin } from '@/client/sdk.gen'
|
|||||||
import type { Client, ClientCreate } from '@/client/types.gen'
|
import type { Client, ClientCreate } from '@/client/types.gen'
|
||||||
|
|
||||||
import { useTreeState } from '@/store/useTreeState'
|
import { useTreeState } from '@/store/useTreeState'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
|
||||||
import ClientTreeItem from '@/components/clients/ClientTreeItem.vue'
|
import ClientTreeItem from '@/components/clients/ClientTreeItem.vue'
|
||||||
import ClientSecretTreeItem from '@/components/clients/ClientSecretTreeItem.vue'
|
import ClientSecretTreeItem from '@/components/clients/ClientSecretTreeItem.vue'
|
||||||
import ClientForm from '@/components/clients/ClientForm.vue'
|
import ClientForm from '@/components/clients/ClientForm.vue'
|
||||||
import PageNumbers from '@/components/common/PageNumbers.vue'
|
import PageNumbers from '@/components/common/PageNumbers.vue'
|
||||||
|
import TreeItemSkeleton from '@/components/common/TreeItemSkeleton.vue'
|
||||||
|
|
||||||
import { useDebounce } from '@/composables/useDebounce'
|
import { useDebounce } from '@/composables/useDebounce'
|
||||||
const treeState = useTreeState()
|
const treeState = useTreeState()
|
||||||
@ -117,7 +124,15 @@ const selectedSecret = ref<string | null>(null)
|
|||||||
const createFormKey = ref<number>(0)
|
const createFormKey = ref<number>(0)
|
||||||
const createDrawerOpen = ref<boolean>(false)
|
const createDrawerOpen = ref<boolean>(false)
|
||||||
|
|
||||||
const clientQuery = ref('')
|
const props = defineProps({
|
||||||
|
loadClient: {
|
||||||
|
type: String,
|
||||||
|
default: '',
|
||||||
|
},
|
||||||
|
})
|
||||||
|
const router = useRouter()
|
||||||
|
|
||||||
|
const clientQuery = toRef(() => props.loadClient)
|
||||||
|
|
||||||
const debouncedQuery = useDebounce(clientQuery, 300)
|
const debouncedQuery = useDebounce(clientQuery, 300)
|
||||||
|
|
||||||
@ -134,49 +149,43 @@ async function loadClients() {
|
|||||||
|
|
||||||
function updateClient(updated: Client) {
|
function updateClient(updated: Client) {
|
||||||
const index = clients.value.findIndex((c) => c.name === updated.name)
|
const index = clients.value.findIndex((c) => c.name === updated.name)
|
||||||
console.log(`UpdateClient fired: ${updated.name} => ${index}`)
|
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
clients.value[index] = updated
|
clients.value[index] = updated
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function itemSelected(event: Event) {
|
function itemSelected(event: Event) {
|
||||||
if (event.detail.selection.length == 0) {
|
if (event.detail.selection) {
|
||||||
treeState.unselect()
|
|
||||||
} else {
|
|
||||||
const el = event.detail.selection[0] as HTMLElement
|
const el = event.detail.selection[0] as HTMLElement
|
||||||
const childType = el.dataset.type
|
const childType = el.dataset.type
|
||||||
if (childType === 'client') {
|
if (childType === 'client') {
|
||||||
const clientId = el.dataset.clientId
|
router.push({ name: 'Client', params: { id: el.dataset.clientId } })
|
||||||
treeState.selectClient(clientId)
|
} else {
|
||||||
} else if (childType == 'secret') {
|
const secretId = el.dataset.name
|
||||||
const secretName = el.dataset.name
|
|
||||||
const parentId = el.dataset.parentId
|
const parentId = el.dataset.parentId
|
||||||
treeState.selectSecret(secretName, parentId)
|
console.log(el.dataset)
|
||||||
|
router.push({
|
||||||
|
name: 'ClientSecret',
|
||||||
|
params: { parentId: el.dataset.parentId, id: el.dataset.name },
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function createClient(data: ClientCreate) {
|
async function createClient(data: ClientCreate) {
|
||||||
const response = await SshecretAdmin.createClientApiV1ClientsPost({ body: data })
|
const response = await SshecretAdmin.createClientApiV1ClientsPost({ body: data })
|
||||||
console.log(response.data)
|
|
||||||
clients.value.unshift(response.data)
|
clients.value.unshift(response.data)
|
||||||
totalClients.value += 1
|
totalClients.value += 1
|
||||||
createDrawerOpen.value = false
|
createDrawerOpen.value = false
|
||||||
createFormKey.value += 1
|
createFormKey.value += 1
|
||||||
treeState.value.selected = true
|
treeState.selectClient(response.data.id)
|
||||||
treeState.value.item_type = 'secret'
|
router.push({ name: 'Client', params: { id: response.data.id } })
|
||||||
treeState.value.client = response.data
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function clientDeleted(id: string) {
|
async function clientDeleted(id: string) {
|
||||||
const index = clients.value.findIndex((c) => c.id === id)
|
const index = clients.value.findIndex((c) => c.id === id)
|
||||||
console.log(`Client Deleted event received: ID: ${id} => ${index}`)
|
|
||||||
if (index >= 0) {
|
if (index >= 0) {
|
||||||
clients.value.splice(index, 1)
|
clients.value.splice(index, 1)
|
||||||
treeState.value.selected = false
|
treeState.unselect()
|
||||||
treeState.value.item_type = null
|
|
||||||
treeState.value.client = null
|
|
||||||
await loadClients()
|
await loadClients()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -196,7 +205,6 @@ async function clearSearch() {
|
|||||||
|
|
||||||
// Watch the search query
|
// Watch the search query
|
||||||
watch(debouncedQuery, async () => {
|
watch(debouncedQuery, async () => {
|
||||||
console.log('Handling search event.')
|
|
||||||
await handleSearchEvent()
|
await handleSearchEvent()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
|||||||
@ -26,7 +26,7 @@
|
|||||||
<section id="detail-pane" class="flex-1 flex overflow-y-auto bg-white p-4 dark:bg-gray-800">
|
<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">
|
<div class="flex flex-col w-full">
|
||||||
<slot name="detail">
|
<slot name="detail">
|
||||||
<p class="p-4 text-gray-500 dark:text-gray-200">Select an item to view details</p>
|
<GenericDetail />
|
||||||
</slot>
|
</slot>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -38,6 +38,7 @@
|
|||||||
import { ref } from 'vue'
|
import { ref } from 'vue'
|
||||||
import '@shoelace-style/shoelace/dist/components/icon/icon.js'
|
import '@shoelace-style/shoelace/dist/components/icon/icon.js'
|
||||||
import Navbar from '@/components/layout/Navbar.vue'
|
import Navbar from '@/components/layout/Navbar.vue'
|
||||||
|
import GenericDetail from '@/components/common/GenericDetail.vue'
|
||||||
|
|
||||||
const masterHidden = ref(true)
|
const masterHidden = ref(true)
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@ -1,13 +1,15 @@
|
|||||||
<template>
|
<template>
|
||||||
<SecretDetail
|
<div>
|
||||||
:secret="secret"
|
<SecretDetail
|
||||||
@update="updateSecretValue"
|
:secret="secret"
|
||||||
@delete="deleteSecret"
|
@update="updateSecretValue"
|
||||||
@addClient="addSecretToClient"
|
@delete="deleteSecret"
|
||||||
@removeClient="removeClientSecret"
|
@addClient="addSecretToClient"
|
||||||
v-if="secret"
|
@removeClient="removeClientSecret"
|
||||||
/>
|
v-if="secret"
|
||||||
<SecretSkeleton v-else />
|
/>
|
||||||
|
<SecretSkeleton v-else />
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch, onMounted } from 'vue'
|
import { ref, watch, onMounted } from 'vue'
|
||||||
@ -18,21 +20,21 @@ import { useTreeState } from '@/store/useTreeState'
|
|||||||
import type { SecretView } from '@/client/types.gen.ts'
|
import type { SecretView } from '@/client/types.gen.ts'
|
||||||
import { SshecretAdmin } from '@/client'
|
import { SshecretAdmin } from '@/client'
|
||||||
|
|
||||||
const props = defineProps<{ secretName: string | null; parentId: string | null }>()
|
const props = defineProps<{ id: string | null; parentId: string | null }>()
|
||||||
const secret = ref<SecretView>()
|
const secret = ref<SecretView>()
|
||||||
|
|
||||||
const treeState = useTreeState()
|
const treeState = useTreeState()
|
||||||
|
|
||||||
async function loadSecret() {
|
async function loadSecret() {
|
||||||
if (!props.secretName) return
|
if (!props.id) return
|
||||||
secret.value = await treeState.getSecret(props.secretName)
|
secret.value = await treeState.getSecret(props.id)
|
||||||
}
|
}
|
||||||
|
|
||||||
async function updateSecretValue(value: string) {
|
async function updateSecretValue(value: string) {
|
||||||
// Update a secret value
|
// Update a secret value
|
||||||
await SshecretAdmin.updateSecretApiV1SecretsNamePut({
|
await SshecretAdmin.updateSecretApiV1SecretsNamePut({
|
||||||
path: {
|
path: {
|
||||||
name: props.secretName,
|
name: props.id,
|
||||||
},
|
},
|
||||||
body: {
|
body: {
|
||||||
value: value,
|
value: value,
|
||||||
@ -46,8 +48,8 @@ async function updateSecretValue(value: string) {
|
|||||||
|
|
||||||
async function deleteSecret(clients: string[]) {
|
async function deleteSecret(clients: string[]) {
|
||||||
// Delete the whole secret
|
// Delete the whole secret
|
||||||
if (props.secretName) {
|
if (props.id) {
|
||||||
await SshecretAdmin.deleteSecretApiV1SecretsNameDelete({ path: { name: props.secretName } })
|
await SshecretAdmin.deleteSecretApiV1SecretsNameDelete({ path: { name: props.id } })
|
||||||
for (const clientId in clients) {
|
for (const clientId in clients) {
|
||||||
await treeState.refreshClient(clientId)
|
await treeState.refreshClient(clientId)
|
||||||
}
|
}
|
||||||
@ -55,12 +57,11 @@ async function deleteSecret(clients: string[]) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function addSecretToClient(clientId: string) {
|
async function addSecretToClient(clientId: string) {
|
||||||
if (props.secretName) {
|
if (props.id) {
|
||||||
console.log('Add Secret to client', props.secretName, clientId)
|
|
||||||
await SshecretAdmin.addSecretToClientApiV1ClientsIdSecretsSecretNamePut({
|
await SshecretAdmin.addSecretToClientApiV1ClientsIdSecretsSecretNamePut({
|
||||||
path: {
|
path: {
|
||||||
id: clientId,
|
id: clientId,
|
||||||
secret_name: props.secretName,
|
secret_name: props.id,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
await treeState.refreshClient(clientId)
|
await treeState.refreshClient(clientId)
|
||||||
@ -69,11 +70,11 @@ async function addSecretToClient(clientId: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function removeClientSecret(clientId: string) {
|
async function removeClientSecret(clientId: string) {
|
||||||
if (props.secretName) {
|
if (props.id) {
|
||||||
await SshecretAdmin.deleteSecretFromClientApiV1ClientsIdSecretsSecretNameDelete({
|
await SshecretAdmin.deleteSecretFromClientApiV1ClientsIdSecretsSecretNameDelete({
|
||||||
path: {
|
path: {
|
||||||
id: clientId,
|
id: clientId,
|
||||||
secret_name: props.secretName,
|
secret_name: props.id,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
await treeState.refreshClient(clientId)
|
await treeState.refreshClient(clientId)
|
||||||
@ -83,7 +84,7 @@ async function removeClientSecret(clientId: string) {
|
|||||||
onMounted(loadSecret)
|
onMounted(loadSecret)
|
||||||
|
|
||||||
watch(
|
watch(
|
||||||
() => props.secretName,
|
() => props.id,
|
||||||
() => loadSecret(),
|
() => loadSecret(),
|
||||||
{ immediate: true },
|
{ immediate: true },
|
||||||
)
|
)
|
||||||
|
|||||||
@ -1,6 +1,12 @@
|
|||||||
<template>
|
<template>
|
||||||
<GroupDetail :group="group" v-if="group" />
|
<div>
|
||||||
<ClientSkeleton v-else />
|
<Transition name="fade" :css="false">
|
||||||
|
<div>
|
||||||
|
<GroupDetail :group="group" v-if="group" />
|
||||||
|
<ClientSkeleton v-else />
|
||||||
|
</div>
|
||||||
|
</Transition>
|
||||||
|
</div>
|
||||||
</template>
|
</template>
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, watch, onMounted } from 'vue'
|
import { ref, watch, onMounted } from 'vue'
|
||||||
|
|||||||
32
packages/sshecret-frontend/src/views/secrets/SecretPage.vue
Normal file
32
packages/sshecret-frontend/src/views/secrets/SecretPage.vue
Normal file
@ -0,0 +1,32 @@
|
|||||||
|
<template>
|
||||||
|
<MasterDetail>
|
||||||
|
<template #master>
|
||||||
|
<MasterTabs selectedTab="secrets" @change="tabSelected" />
|
||||||
|
</template>
|
||||||
|
<template #detail>
|
||||||
|
<RouterView v-slot="{ Component, route }">
|
||||||
|
<transition name="fade" :css="false">
|
||||||
|
<component :is="Component" :key="route.path" />
|
||||||
|
</transition>
|
||||||
|
</RouterView>
|
||||||
|
</template>
|
||||||
|
</MasterDetail>
|
||||||
|
</template>
|
||||||
|
<script setup lang="ts">
|
||||||
|
import { computed } from 'vue'
|
||||||
|
import MasterDetail from '@/views/layout/MasterDetail.vue'
|
||||||
|
import MasterTabs from '@/components/common/MasterTabs.vue'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
|
import { RouterView } from 'vue-router'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
|
const route = useRoute()
|
||||||
|
|
||||||
|
function tabSelected(tabName: string) {
|
||||||
|
if (tabName !== 'secrets') {
|
||||||
|
router.push({ name: tabName })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const routeKey = computed(() => route.name + '-' + (route.params.id ?? 'root'))
|
||||||
|
</script>
|
||||||
@ -34,6 +34,11 @@
|
|||||||
<SecretGroup v-for="group in secretGroups" :group="group" />
|
<SecretGroup v-for="group in secretGroups" :group="group" />
|
||||||
</template>
|
</template>
|
||||||
</sl-tree>
|
</sl-tree>
|
||||||
|
<sl-tree v-else>
|
||||||
|
<template v-for="n in 10">
|
||||||
|
<TreeItemSkeleton />
|
||||||
|
</template>
|
||||||
|
</sl-tree>
|
||||||
</div>
|
</div>
|
||||||
<!-- pagination would go here -->
|
<!-- pagination would go here -->
|
||||||
</div>
|
</div>
|
||||||
@ -60,17 +65,21 @@
|
|||||||
import type { SshecretObject } from '@/api/types'
|
import type { SshecretObject } from '@/api/types'
|
||||||
import type { SecretCreate } from '@/client'
|
import type { SecretCreate } from '@/client'
|
||||||
import { computed, ref, reactive, onMounted, watch } from 'vue'
|
import { computed, ref, reactive, onMounted, watch } from 'vue'
|
||||||
|
import { useRouter, useRoute } from 'vue-router'
|
||||||
import { useTreeState } from '@/store/useTreeState'
|
import { useTreeState } from '@/store/useTreeState'
|
||||||
import { useAlertsStore } from '@/store/useAlertsStore'
|
import { useAlertsStore } from '@/store/useAlertsStore'
|
||||||
import { SshecretAdmin } from '@/client'
|
import { SshecretAdmin } from '@/client'
|
||||||
import { SshecretObjectType } from '@/api/types'
|
import { SshecretObjectType } from '@/api/types'
|
||||||
|
import { splitPath } from '@/api/paths'
|
||||||
import SecretGroup from '@/components/secrets/SecretGroup.vue'
|
import SecretGroup from '@/components/secrets/SecretGroup.vue'
|
||||||
import SecretGroupTreeItem from '@/components/secrets/SecretGroupTreeItem.vue'
|
import SecretGroupTreeItem from '@/components/secrets/SecretGroupTreeItem.vue'
|
||||||
import SecretGroupTreeEntry from '@/components/secrets/SecretGroupTreeEntry.vue'
|
import SecretGroupTreeEntry from '@/components/secrets/SecretGroupTreeEntry.vue'
|
||||||
import AddGroup from '@/components/secrets/AddGroup.vue'
|
import AddGroup from '@/components/secrets/AddGroup.vue'
|
||||||
import SecretForm from '@/components/secrets/SecretForm.vue'
|
import SecretForm from '@/components/secrets/SecretForm.vue'
|
||||||
import Drawer from '@/components/common/Drawer.vue'
|
import Drawer from '@/components/common/Drawer.vue'
|
||||||
|
import TreeItemSkeleton from '@/components/common/TreeItemSkeleton.vue'
|
||||||
|
|
||||||
|
const router = useRouter()
|
||||||
const treeState = useTreeState()
|
const treeState = useTreeState()
|
||||||
const alerts = useAlertsStore()
|
const alerts = useAlertsStore()
|
||||||
|
|
||||||
@ -126,12 +135,15 @@ async function itemSelected(event: Event) {
|
|||||||
if (childType === 'secret') {
|
if (childType === 'secret') {
|
||||||
const secretName = el.dataset.name
|
const secretName = el.dataset.name
|
||||||
treeState.selectSecret(secretName, null)
|
treeState.selectSecret(secretName, null)
|
||||||
|
router.push({ name: 'Secret', params: { id: secretName } })
|
||||||
} else if (childType === 'group') {
|
} else if (childType === 'group') {
|
||||||
const groupPath = el.dataset.groupPath
|
const groupPath = el.dataset.groupPath
|
||||||
if (groupPath === 'ungrouped') {
|
if (groupPath === 'ungrouped') {
|
||||||
treeState.unselect()
|
treeState.unselect()
|
||||||
} else {
|
} else {
|
||||||
|
const groupPathElements = splitPath(groupPath)
|
||||||
treeState.selectGroup(groupPath)
|
treeState.selectGroup(groupPath)
|
||||||
|
router.push({ name: 'Group', params: { groupPath: groupPathElements } })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -139,14 +151,12 @@ async function itemSelected(event: Event) {
|
|||||||
|
|
||||||
async function createGroup(path: string) {
|
async function createGroup(path: string) {
|
||||||
// Create a group
|
// Create a group
|
||||||
console.log('Submit called')
|
|
||||||
const response = await SshecretAdmin.addSecretGroupApiV1SecretsGroupsPost({
|
const response = await SshecretAdmin.addSecretGroupApiV1SecretsGroupsPost({
|
||||||
body: {
|
body: {
|
||||||
name: path,
|
name: path,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
if (response.status === 200) {
|
if (response.status === 200) {
|
||||||
console.log('Success. Group created.')
|
|
||||||
alerts.showAlert('Group created', 'success')
|
alerts.showAlert('Group created', 'success')
|
||||||
createGroupDrawer.value = false
|
createGroupDrawer.value = false
|
||||||
drawerKey.value += 1
|
drawerKey.value += 1
|
||||||
@ -159,7 +169,6 @@ async function createGroup(path: string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function createSecret(secretCreate: SecretCreate) {
|
async function createSecret(secretCreate: SecretCreate) {
|
||||||
console.log('Creating secret')
|
|
||||||
const response = await SshecretAdmin.addSecretApiV1SecretsPost({
|
const response = await SshecretAdmin.addSecretApiV1SecretsPost({
|
||||||
body: secretCreate,
|
body: secretCreate,
|
||||||
})
|
})
|
||||||
@ -172,13 +181,12 @@ async function createSecret(secretCreate: SecretCreate) {
|
|||||||
await loadGroups()
|
await loadGroups()
|
||||||
// Also update all the clients affected
|
// Also update all the clients affected
|
||||||
for (const clientId in secretCreate.clients) {
|
for (const clientId in secretCreate.clients) {
|
||||||
console.log('Refreshing client: ', clientId)
|
|
||||||
await treeState.refreshClient(clientId)
|
await treeState.refreshClient(clientId)
|
||||||
}
|
}
|
||||||
|
|
||||||
treeState.selectSecret(secretCreate.name)
|
treeState.selectSecret(secretCreate.name)
|
||||||
} else {
|
} else {
|
||||||
console.log(response)
|
console.error(response)
|
||||||
alerts.showAlert('Secret creation failed', 'error')
|
alerts.showAlert('Secret creation failed', 'error')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user