mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-02-11 02:34:18 +00:00
feat: add various improvements to the table component (#961)
Co-authored-by: Kyle Mendell <kmendell@ofkm.us>
This commit is contained in:
@@ -1,8 +1,9 @@
|
||||
import versionService from '$lib/services/version-service';
|
||||
import VersionService from '$lib/services/version-service';
|
||||
import type { AppVersionInformation } from '$lib/types/application-configuration';
|
||||
import type { LayoutLoad } from './$types';
|
||||
|
||||
export const load: LayoutLoad = async () => {
|
||||
const versionService = new VersionService();
|
||||
const currentVersion = versionService.getCurrentVersion();
|
||||
|
||||
let newestVersion = null;
|
||||
|
||||
@@ -12,22 +12,16 @@
|
||||
import ApiKeyForm from './api-key-form.svelte';
|
||||
import ApiKeyList from './api-key-list.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
let apiKeys = $state(data.apiKeys);
|
||||
let apiKeysRequestOptions = $state(data.apiKeysRequestOptions);
|
||||
|
||||
const apiKeyService = new ApiKeyService();
|
||||
let expandAddApiKey = $state(false);
|
||||
let apiKeyResponse = $state<ApiKeyResponse | null>(null);
|
||||
let listRef: ApiKeyList;
|
||||
|
||||
async function createApiKey(apiKeyData: ApiKeyCreate) {
|
||||
try {
|
||||
const response = await apiKeyService.create(apiKeyData);
|
||||
apiKeyResponse = response;
|
||||
|
||||
// After creation, reload the list of API keys
|
||||
apiKeys = await apiKeyService.list(apiKeysRequestOptions);
|
||||
|
||||
listRef.refresh();
|
||||
return true;
|
||||
} catch (e) {
|
||||
axiosErrorToast(e);
|
||||
@@ -40,52 +34,46 @@
|
||||
<title>{m.api_keys()}</title>
|
||||
</svelte:head>
|
||||
|
||||
<div>
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Card.Title>
|
||||
<ShieldPlus class="text-primary/80 size-5" />
|
||||
{m.create_api_key()}
|
||||
</Card.Title>
|
||||
<Card.Description
|
||||
><FormattedMessage
|
||||
m={m.add_a_new_api_key_for_programmatic_access()}
|
||||
/></Card.Description
|
||||
>
|
||||
</div>
|
||||
{#if !expandAddApiKey}
|
||||
<Button onclick={() => (expandAddApiKey = true)}>{m.add_api_key()}</Button>
|
||||
{:else}
|
||||
<Button class="h-8 p-3" variant="ghost" onclick={() => (expandAddApiKey = false)}>
|
||||
<LucideMinus class="size-5" />
|
||||
</Button>
|
||||
{/if}
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<div class="flex items-center justify-between">
|
||||
<div>
|
||||
<Card.Title>
|
||||
<ShieldPlus class="text-primary/80 size-5" />
|
||||
{m.create_api_key()}
|
||||
</Card.Title>
|
||||
<Card.Description
|
||||
><FormattedMessage m={m.add_a_new_api_key_for_programmatic_access()} /></Card.Description
|
||||
>
|
||||
</div>
|
||||
</Card.Header>
|
||||
{#if expandAddApiKey}
|
||||
<div transition:slide>
|
||||
<Card.Content>
|
||||
<ApiKeyForm callback={createApiKey} />
|
||||
</Card.Content>
|
||||
</div>
|
||||
{/if}
|
||||
</Card.Root>
|
||||
</div>
|
||||
{#if !expandAddApiKey}
|
||||
<Button onclick={() => (expandAddApiKey = true)}>{m.add_api_key()}</Button>
|
||||
{:else}
|
||||
<Button class="h-8 p-3" variant="ghost" onclick={() => (expandAddApiKey = false)}>
|
||||
<LucideMinus class="size-5" />
|
||||
</Button>
|
||||
{/if}
|
||||
</div>
|
||||
</Card.Header>
|
||||
{#if expandAddApiKey}
|
||||
<div transition:slide>
|
||||
<Card.Content>
|
||||
<ApiKeyForm callback={createApiKey} />
|
||||
</Card.Content>
|
||||
</div>
|
||||
{/if}
|
||||
</Card.Root>
|
||||
|
||||
<div>
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>
|
||||
<ShieldEllipsis class="text-primary/80 size-5" />
|
||||
{m.manage_api_keys()}
|
||||
</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<ApiKeyList {apiKeys} requestOptions={apiKeysRequestOptions} />
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</div>
|
||||
<Card.Root class="gap-0">
|
||||
<Card.Header>
|
||||
<Card.Title>
|
||||
<ShieldEllipsis class="text-primary/80 size-5" />
|
||||
{m.manage_api_keys()}
|
||||
</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<ApiKeyList bind:this={listRef} />
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<ApiKeyDialog bind:apiKeyResponse />
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import ApiKeyService from '$lib/services/api-key-service';
|
||||
import type { SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async () => {
|
||||
const apiKeyService = new ApiKeyService();
|
||||
|
||||
const apiKeysRequestOptions: SearchPaginationSortRequest = {
|
||||
sort: {
|
||||
column: 'lastUsedAt',
|
||||
direction: 'desc' as const
|
||||
}
|
||||
};
|
||||
|
||||
const apiKeys = await apiKeyService.list(apiKeysRequestOptions);
|
||||
|
||||
return { apiKeys, apiKeysRequestOptions };
|
||||
};
|
||||
@@ -1,31 +1,66 @@
|
||||
<script lang="ts">
|
||||
import AdvancedTable from '$lib/components/advanced-table.svelte';
|
||||
import { openConfirmDialog } from '$lib/components/confirm-dialog';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
import AdvancedTable from '$lib/components/table/advanced-table.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import ApiKeyService from '$lib/services/api-key-service';
|
||||
import type {
|
||||
AdvancedTableColumn,
|
||||
CreateAdvancedTableActions
|
||||
} from '$lib/types/advanced-table.type';
|
||||
import type { ApiKey } from '$lib/types/api-key.type';
|
||||
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||
import { LucideBan } from '@lucide/svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
let {
|
||||
apiKeys,
|
||||
requestOptions
|
||||
}: {
|
||||
apiKeys: Paginated<ApiKey>;
|
||||
requestOptions: SearchPaginationSortRequest;
|
||||
} = $props();
|
||||
|
||||
const apiKeyService = new ApiKeyService();
|
||||
|
||||
let tableRef: AdvancedTable<ApiKey>;
|
||||
|
||||
export function refresh() {
|
||||
return tableRef?.refresh();
|
||||
}
|
||||
|
||||
function formatDate(dateStr: string | undefined) {
|
||||
if (!dateStr) return m.never();
|
||||
return new Date(dateStr).toLocaleString();
|
||||
}
|
||||
|
||||
const columns: AdvancedTableColumn<ApiKey>[] = [
|
||||
{ label: m.name(), column: 'name', sortable: true },
|
||||
{
|
||||
label: m.description(),
|
||||
column: 'description'
|
||||
},
|
||||
{
|
||||
label: m.expires_at(),
|
||||
column: 'expiresAt',
|
||||
sortable: true,
|
||||
value: (item) => formatDate(item.expiresAt)
|
||||
},
|
||||
{
|
||||
label: m.last_used(),
|
||||
column: 'lastUsedAt',
|
||||
sortable: true,
|
||||
value: (item) => formatDate(item.lastUsedAt)
|
||||
},
|
||||
{
|
||||
label: m.created(),
|
||||
column: 'createdAt',
|
||||
sortable: true,
|
||||
hidden: true,
|
||||
value: (item) => formatDate(item.createdAt)
|
||||
}
|
||||
];
|
||||
|
||||
const actions: CreateAdvancedTableActions<ApiKey> = (apiKey) => [
|
||||
{
|
||||
label: m.revoke(),
|
||||
icon: LucideBan,
|
||||
variant: 'danger',
|
||||
onClick: (apiKey) => revokeApiKey(apiKey)
|
||||
}
|
||||
];
|
||||
|
||||
function revokeApiKey(apiKey: ApiKey) {
|
||||
openConfirmDialog({
|
||||
title: m.revoke_api_key(),
|
||||
@@ -38,7 +73,7 @@
|
||||
action: async () => {
|
||||
try {
|
||||
await apiKeyService.revoke(apiKey.id);
|
||||
apiKeys = await apiKeyService.list(requestOptions);
|
||||
await refresh();
|
||||
toast.success(m.api_key_revoked_successfully());
|
||||
} catch (e) {
|
||||
axiosErrorToast(e);
|
||||
@@ -50,27 +85,11 @@
|
||||
</script>
|
||||
|
||||
<AdvancedTable
|
||||
items={apiKeys}
|
||||
{requestOptions}
|
||||
onRefresh={async (o) => (apiKeys = await apiKeyService.list(o))}
|
||||
id="api-key-list"
|
||||
bind:this={tableRef}
|
||||
fetchCallback={apiKeyService.list}
|
||||
defaultSort={{ column: 'lastUsedAt', direction: 'desc' }}
|
||||
withoutSearch
|
||||
columns={[
|
||||
{ label: m.name(), sortColumn: 'name' },
|
||||
{ label: m.description() },
|
||||
{ label: m.expires_at(), sortColumn: 'expiresAt' },
|
||||
{ label: m.last_used(), sortColumn: 'lastUsedAt' },
|
||||
{ label: m.actions(), hidden: true }
|
||||
]}
|
||||
>
|
||||
{#snippet rows({ item })}
|
||||
<Table.Cell>{item.name}</Table.Cell>
|
||||
<Table.Cell class="text-muted-foreground">{item.description || '-'}</Table.Cell>
|
||||
<Table.Cell>{formatDate(item.expiresAt)}</Table.Cell>
|
||||
<Table.Cell>{formatDate(item.lastUsedAt)}</Table.Cell>
|
||||
<Table.Cell class="flex justify-end">
|
||||
<Button onclick={() => revokeApiKey(item)} size="sm" variant="outline" aria-label={m.revoke()}
|
||||
><LucideBan class="size-3 text-red-500" /></Button
|
||||
>
|
||||
</Table.Cell>
|
||||
{/snippet}
|
||||
</AdvancedTable>
|
||||
{columns}
|
||||
{actions}
|
||||
/>
|
||||
|
||||
@@ -32,7 +32,7 @@
|
||||
/>
|
||||
<ApplicationImage
|
||||
id="logo-light"
|
||||
imageClass="size-32"
|
||||
imageClass="size-24"
|
||||
label={m.light_mode_logo()}
|
||||
bind:image={logoLight}
|
||||
imageURL={cachedApplicationLogo.getUrl(true)}
|
||||
@@ -40,7 +40,7 @@
|
||||
/>
|
||||
<ApplicationImage
|
||||
id="logo-dark"
|
||||
imageClass="size-32"
|
||||
imageClass="size-24"
|
||||
label={m.dark_mode_logo()}
|
||||
bind:image={logoDark}
|
||||
imageURL={cachedApplicationLogo.getUrl(false)}
|
||||
|
||||
@@ -14,9 +14,6 @@
|
||||
import OIDCClientForm from './oidc-client-form.svelte';
|
||||
import OIDCClientList from './oidc-client-list.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
let clients = $state(data.clients);
|
||||
let clientsRequestOptions = $state(data.clientsRequestOptions);
|
||||
let expandAddClient = $state(false);
|
||||
|
||||
const oidcService = new OIDCService();
|
||||
@@ -86,7 +83,7 @@
|
||||
</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<OIDCClientList {clients} requestOptions={clientsRequestOptions} />
|
||||
<OIDCClientList />
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</div>
|
||||
|
||||
@@ -1,18 +0,0 @@
|
||||
import OIDCService from '$lib/services/oidc-service';
|
||||
import type { SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async () => {
|
||||
const oidcService = new OIDCService();
|
||||
|
||||
const clientsRequestOptions: SearchPaginationSortRequest = {
|
||||
sort: {
|
||||
column: 'name',
|
||||
direction: 'asc'
|
||||
}
|
||||
};
|
||||
|
||||
const clients = await oidcService.listClients(clientsRequestOptions);
|
||||
|
||||
return { clients, clientsRequestOptions };
|
||||
};
|
||||
@@ -16,6 +16,7 @@
|
||||
import { LucideChevronLeft, LucideRefreshCcw } from '@lucide/svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { backNavigate } from '../../users/navigate-back-util';
|
||||
import OidcForm from '../oidc-client-form.svelte';
|
||||
import OidcClientPreviewModal from '../oidc-client-preview-modal.svelte';
|
||||
|
||||
@@ -28,6 +29,7 @@
|
||||
let showPreview = $state(false);
|
||||
|
||||
const oidcService = new OidcService();
|
||||
const backNavigation = backNavigate('/settings/admin/oidc-clients');
|
||||
|
||||
const setupDetails = $state({
|
||||
[m.authorization_url()]: `https://${page.url.host}/authorize`,
|
||||
@@ -107,8 +109,8 @@
|
||||
</svelte:head>
|
||||
|
||||
<div>
|
||||
<a class="text-muted-foreground flex text-sm" href="/settings/admin/oidc-clients"
|
||||
><LucideChevronLeft class="size-5" /> {m.back()}</a
|
||||
<button type="button" class="text-muted-foreground flex text-sm" onclick={backNavigation.go}
|
||||
><LucideChevronLeft class="size-5" /> {m.back()}</button
|
||||
>
|
||||
</div>
|
||||
<Card.Root>
|
||||
|
||||
@@ -1,27 +1,82 @@
|
||||
<script lang="ts">
|
||||
import AdvancedTable from '$lib/components/advanced-table.svelte';
|
||||
import { goto } from '$app/navigation';
|
||||
import { openConfirmDialog } from '$lib/components/confirm-dialog/';
|
||||
import ImageBox from '$lib/components/image-box.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
import AdvancedTable from '$lib/components/table/advanced-table.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import OIDCService from '$lib/services/oidc-service';
|
||||
import type {
|
||||
AdvancedTableColumn,
|
||||
CreateAdvancedTableActions
|
||||
} from '$lib/types/advanced-table.type';
|
||||
import type { OidcClient, OidcClientWithAllowedUserGroupsCount } from '$lib/types/oidc.type';
|
||||
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||
import { cachedOidcClientLogo } from '$lib/utils/cached-image-util';
|
||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||
import { LucidePencil, LucideTrash } from '@lucide/svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
let {
|
||||
clients = $bindable(),
|
||||
requestOptions
|
||||
}: {
|
||||
clients: Paginated<OidcClientWithAllowedUserGroupsCount>;
|
||||
requestOptions: SearchPaginationSortRequest;
|
||||
} = $props();
|
||||
|
||||
const oidcService = new OIDCService();
|
||||
let tableRef: AdvancedTable<OidcClientWithAllowedUserGroupsCount>;
|
||||
|
||||
export function refresh() {
|
||||
return tableRef?.refresh();
|
||||
}
|
||||
|
||||
const booleanFilterValues = [
|
||||
{ label: m.enabled(), value: true },
|
||||
{ label: m.disabled(), value: false }
|
||||
];
|
||||
|
||||
const columns: AdvancedTableColumn<OidcClientWithAllowedUserGroupsCount>[] = [
|
||||
{ label: 'ID', column: 'id', hidden: true },
|
||||
{ label: m.logo(), key: 'logo', cell: LogoCell },
|
||||
{ label: m.name(), column: 'name', sortable: true },
|
||||
{
|
||||
label: m.oidc_allowed_group_count(),
|
||||
column: 'allowedUserGroupsCount',
|
||||
sortable: true,
|
||||
value: (item) =>
|
||||
item.allowedUserGroupsCount > 0 ? item.allowedUserGroupsCount : m.unrestricted()
|
||||
},
|
||||
{
|
||||
label: m.pkce(),
|
||||
column: 'pkceEnabled',
|
||||
sortable: true,
|
||||
hidden: true,
|
||||
filterableValues: booleanFilterValues
|
||||
},
|
||||
{
|
||||
label: m.reauthentication(),
|
||||
column: 'requiresReauthentication',
|
||||
sortable: true,
|
||||
filterableValues: booleanFilterValues
|
||||
},
|
||||
{
|
||||
label: m.client_launch_url(),
|
||||
column: 'launchURL',
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
label: m.public_client(),
|
||||
column: 'isPublic',
|
||||
sortable: true,
|
||||
hidden: true
|
||||
}
|
||||
];
|
||||
|
||||
const actions: CreateAdvancedTableActions<OidcClientWithAllowedUserGroupsCount> = (_) => [
|
||||
{
|
||||
label: m.edit(),
|
||||
icon: LucidePencil,
|
||||
onClick: (client) => goto(`/settings/admin/oidc-clients/${client.id}`)
|
||||
},
|
||||
{
|
||||
label: m.delete(),
|
||||
icon: LucideTrash,
|
||||
variant: 'danger',
|
||||
onClick: (client) => deleteClient(client)
|
||||
}
|
||||
];
|
||||
|
||||
async function deleteClient(client: OidcClient) {
|
||||
openConfirmDialog({
|
||||
@@ -33,7 +88,7 @@
|
||||
action: async () => {
|
||||
try {
|
||||
await oidcService.removeClient(client.id);
|
||||
clients = await oidcService.listClients(requestOptions!);
|
||||
await refresh();
|
||||
toast.success(m.oidc_client_deleted_successfully());
|
||||
} catch (e) {
|
||||
axiosErrorToast(e);
|
||||
@@ -44,48 +99,25 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#snippet LogoCell({ item }: { item: OidcClientWithAllowedUserGroupsCount })}
|
||||
{#if item.hasLogo}
|
||||
<ImageBox
|
||||
class="size-12 rounded-lg"
|
||||
src={cachedOidcClientLogo.getUrl(item.id)}
|
||||
alt={m.name_logo({ name: item.name })}
|
||||
/>
|
||||
{:else}
|
||||
<div class="bg-muted flex size-12 items-center justify-center rounded-lg text-lg font-bold">
|
||||
{item.name.charAt(0).toUpperCase()}
|
||||
</div>
|
||||
{/if}
|
||||
{/snippet}
|
||||
|
||||
<AdvancedTable
|
||||
items={clients}
|
||||
{requestOptions}
|
||||
onRefresh={async (o) => (clients = await oidcService.listClients(o))}
|
||||
columns={[
|
||||
{ label: m.logo() },
|
||||
{ label: m.name(), sortColumn: 'name' },
|
||||
{ label: m.oidc_allowed_group_count(), sortColumn: 'allowedUserGroupsCount' },
|
||||
{ label: m.actions(), hidden: true }
|
||||
]}
|
||||
>
|
||||
{#snippet rows({ item })}
|
||||
<Table.Cell class="w-8 font-medium">
|
||||
{#if item.hasLogo}
|
||||
<ImageBox
|
||||
class="min-h-8 min-w-8"
|
||||
src={cachedOidcClientLogo.getUrl(item.id)}
|
||||
alt={m.name_logo({ name: item.name })}
|
||||
/>
|
||||
{/if}
|
||||
</Table.Cell>
|
||||
<Table.Cell class="font-medium">{item.name}</Table.Cell>
|
||||
<Table.Cell class="font-medium"
|
||||
>{item.allowedUserGroupsCount > 0
|
||||
? item.allowedUserGroupsCount
|
||||
: m.unrestricted()}</Table.Cell
|
||||
>
|
||||
<Table.Cell class="align-middle">
|
||||
<div class="flex justify-end gap-1">
|
||||
<Button
|
||||
href="/settings/admin/oidc-clients/{item.id}"
|
||||
size="sm"
|
||||
variant="outline"
|
||||
aria-label={m.edit()}><LucidePencil class="size-3 " /></Button
|
||||
>
|
||||
<Button
|
||||
onclick={() => deleteClient(item)}
|
||||
size="sm"
|
||||
variant="outline"
|
||||
aria-label={m.delete()}><LucideTrash class="size-3 text-red-500" /></Button
|
||||
>
|
||||
</div>
|
||||
</Table.Cell>
|
||||
{/snippet}
|
||||
</AdvancedTable>
|
||||
id="oidc-client-list"
|
||||
bind:this={tableRef}
|
||||
fetchCallback={oidcService.listClients}
|
||||
defaultSort={{ column: 'name', direction: 'asc' }}
|
||||
{columns}
|
||||
{actions}
|
||||
/>
|
||||
|
||||
@@ -12,9 +12,6 @@
|
||||
import UserGroupForm from './user-group-form.svelte';
|
||||
import UserGroupList from './user-group-list.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
let userGroups = $state(data.userGroups);
|
||||
let userGroupsRequestOptions = $state(data.userGroupsRequestOptions);
|
||||
let expandAddUserGroup = $state(false);
|
||||
|
||||
const userGroupService = new UserGroupService();
|
||||
@@ -79,7 +76,7 @@
|
||||
</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<UserGroupList {userGroups} requestOptions={userGroupsRequestOptions} />
|
||||
<UserGroupList />
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</div>
|
||||
|
||||
@@ -1,17 +0,0 @@
|
||||
import UserGroupService from '$lib/services/user-group-service';
|
||||
import type { SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async () => {
|
||||
const userGroupService = new UserGroupService();
|
||||
|
||||
const userGroupsRequestOptions: SearchPaginationSortRequest = {
|
||||
sort: {
|
||||
column: 'friendlyName',
|
||||
direction: 'asc'
|
||||
}
|
||||
};
|
||||
|
||||
const userGroups = await userGroupService.list(userGroupsRequestOptions);
|
||||
return { userGroups, userGroupsRequestOptions };
|
||||
};
|
||||
@@ -4,6 +4,7 @@
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import CustomClaimService from '$lib/services/custom-claim-service';
|
||||
import UserGroupService from '$lib/services/user-group-service';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
@@ -11,9 +12,9 @@
|
||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||
import { LucideChevronLeft } from '@lucide/svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { backNavigate } from '../../users/navigate-back-util';
|
||||
import UserGroupForm from '../user-group-form.svelte';
|
||||
import UserSelection from '../user-selection.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
|
||||
let { data } = $props();
|
||||
let userGroup = $state({
|
||||
@@ -23,6 +24,7 @@
|
||||
|
||||
const userGroupService = new UserGroupService();
|
||||
const customClaimService = new CustomClaimService();
|
||||
const backNavigation = backNavigate('/settings/admin/user-groups');
|
||||
|
||||
async function updateUserGroup(updatedUserGroup: UserGroupCreate) {
|
||||
let success = true;
|
||||
@@ -61,8 +63,8 @@
|
||||
</svelte:head>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<a class="text-muted-foreground flex text-sm" href="/settings/admin/user-groups"
|
||||
><LucideChevronLeft class="size-5" /> {m.back()}</a
|
||||
<button type="button" class="text-muted-foreground flex text-sm" onclick={backNavigation.go}
|
||||
><LucideChevronLeft class="size-5" /> {m.back()}</button
|
||||
>
|
||||
{#if !!userGroup.ldapId}
|
||||
<Badge class="rounded-full" variant="default">{m.ldap()}</Badge>
|
||||
|
||||
@@ -1,29 +1,58 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import AdvancedTable from '$lib/components/advanced-table.svelte';
|
||||
import { openConfirmDialog } from '$lib/components/confirm-dialog/';
|
||||
import AdvancedTable from '$lib/components/table/advanced-table.svelte';
|
||||
import { Badge } from '$lib/components/ui/badge/index';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import UserGroupService from '$lib/services/user-group-service';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||
import type {
|
||||
AdvancedTableColumn,
|
||||
CreateAdvancedTableActions
|
||||
} from '$lib/types/advanced-table.type';
|
||||
import type { UserGroup, UserGroupWithUserCount } from '$lib/types/user-group.type';
|
||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||
import { LucidePencil, LucideTrash } from '@lucide/svelte';
|
||||
import Ellipsis from '@lucide/svelte/icons/ellipsis';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
let {
|
||||
userGroups,
|
||||
requestOptions
|
||||
}: {
|
||||
userGroups: Paginated<UserGroupWithUserCount>;
|
||||
requestOptions: SearchPaginationSortRequest;
|
||||
} = $props();
|
||||
|
||||
const userGroupService = new UserGroupService();
|
||||
let tableRef: AdvancedTable<UserGroupWithUserCount>;
|
||||
|
||||
export function refresh() {
|
||||
return tableRef?.refresh();
|
||||
}
|
||||
|
||||
const columns: AdvancedTableColumn<UserGroupWithUserCount>[] = [
|
||||
{ label: 'ID', column: 'id', hidden: true },
|
||||
{ label: m.friendly_name(), column: 'friendlyName', sortable: true },
|
||||
{ label: m.name(), column: 'name', sortable: true },
|
||||
{ label: m.user_count(), column: 'userCount', sortable: true },
|
||||
{
|
||||
label: m.created(),
|
||||
column: 'createdAt',
|
||||
sortable: true,
|
||||
hidden: true,
|
||||
value: (item) => new Date(item.createdAt).toLocaleString()
|
||||
},
|
||||
{ label: m.ldap_id(), column: 'ldapId', hidden: true },
|
||||
{ label: m.source(), key: 'source', hidden: !$appConfigStore.ldapEnabled, cell: SourceCell }
|
||||
];
|
||||
|
||||
const actions: CreateAdvancedTableActions<UserGroupWithUserCount> = (group) => [
|
||||
{
|
||||
label: m.edit(),
|
||||
icon: LucidePencil,
|
||||
variant: 'ghost',
|
||||
onClick: (group) => goto(`/settings/admin/user-groups/${group.id}`)
|
||||
},
|
||||
{
|
||||
label: m.delete(),
|
||||
icon: LucideTrash,
|
||||
variant: 'danger',
|
||||
onClick: (group) => deleteUserGroup(group),
|
||||
visible: group.ldapId || $appConfigStore.ldapEnabled
|
||||
}
|
||||
];
|
||||
|
||||
async function deleteUserGroup(userGroup: UserGroup) {
|
||||
openConfirmDialog({
|
||||
@@ -35,7 +64,7 @@
|
||||
action: async () => {
|
||||
try {
|
||||
await userGroupService.remove(userGroup.id);
|
||||
userGroups = await userGroupService.list(requestOptions!);
|
||||
await refresh();
|
||||
toast.success(m.user_group_deleted_successfully());
|
||||
} catch (e) {
|
||||
axiosErrorToast(e);
|
||||
@@ -46,48 +75,17 @@
|
||||
}
|
||||
</script>
|
||||
|
||||
{#snippet SourceCell({ item }: { item: UserGroupWithUserCount })}
|
||||
<Badge class="rounded-full" variant={item.ldapId ? 'default' : 'outline'}>
|
||||
{item.ldapId ? m.ldap() : m.local()}
|
||||
</Badge>
|
||||
{/snippet}
|
||||
|
||||
<AdvancedTable
|
||||
items={userGroups}
|
||||
onRefresh={async (o) => (userGroups = await userGroupService.list(o))}
|
||||
{requestOptions}
|
||||
columns={[
|
||||
{ label: m.friendly_name(), sortColumn: 'friendlyName' },
|
||||
{ label: m.name(), sortColumn: 'name' },
|
||||
{ label: m.user_count(), sortColumn: 'userCount' },
|
||||
...($appConfigStore.ldapEnabled ? [{ label: m.source() }] : []),
|
||||
{ label: m.actions(), hidden: true }
|
||||
]}
|
||||
>
|
||||
{#snippet rows({ item })}
|
||||
<Table.Cell>{item.friendlyName}</Table.Cell>
|
||||
<Table.Cell>{item.name}</Table.Cell>
|
||||
<Table.Cell>{item.userCount}</Table.Cell>
|
||||
{#if $appConfigStore.ldapEnabled}
|
||||
<Table.Cell>
|
||||
<Badge class="rounded-full" variant={item.ldapId ? 'default' : 'outline'}
|
||||
>{item.ldapId ? m.ldap() : m.local()}</Badge
|
||||
>
|
||||
</Table.Cell>
|
||||
{/if}
|
||||
<Table.Cell class="flex justify-end">
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger>
|
||||
<Ellipsis class="size-4" />
|
||||
<span class="sr-only">{m.toggle_menu()}</span>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end">
|
||||
<DropdownMenu.Item onclick={() => goto(`/settings/admin/user-groups/${item.id}`)}
|
||||
><LucidePencil class="mr-2 size-4" /> {m.edit()}</DropdownMenu.Item
|
||||
>
|
||||
{#if !item.ldapId || !$appConfigStore.ldapEnabled}
|
||||
<DropdownMenu.Item
|
||||
class="text-red-500 focus:!text-red-700"
|
||||
onclick={() => deleteUserGroup(item)}
|
||||
><LucideTrash class="mr-2 size-4" />{m.delete()}</DropdownMenu.Item
|
||||
>
|
||||
{/if}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</Table.Cell>
|
||||
{/snippet}
|
||||
</AdvancedTable>
|
||||
id="user-group-list"
|
||||
bind:this={tableRef}
|
||||
fetchCallback={userGroupService.list}
|
||||
defaultSort={{ column: 'friendlyName', direction: 'asc' }}
|
||||
{columns}
|
||||
{actions}
|
||||
/>
|
||||
|
||||
@@ -1,11 +1,12 @@
|
||||
<script lang="ts">
|
||||
import AdvancedTable from '$lib/components/advanced-table.svelte';
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
import AdvancedTable from '$lib/components/table/advanced-table.svelte';
|
||||
import * as Avatar from '$lib/components/ui/avatar/index';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import UserService from '$lib/services/user-service';
|
||||
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||
import type { AdvancedTableColumn } from '$lib/types/advanced-table.type';
|
||||
import type { User } from '$lib/types/user.type';
|
||||
import { onMount } from 'svelte';
|
||||
import { cachedProfilePicture } from '$lib/utils/cached-image-util';
|
||||
|
||||
let {
|
||||
selectionDisabled = false,
|
||||
@@ -17,34 +18,63 @@
|
||||
|
||||
const userService = new UserService();
|
||||
|
||||
let users: Paginated<User> | undefined = $state();
|
||||
let requestOptions: SearchPaginationSortRequest = $state({
|
||||
sort: {
|
||||
column: 'firstName',
|
||||
direction: 'asc'
|
||||
}
|
||||
});
|
||||
|
||||
onMount(async () => {
|
||||
users = await userService.list(requestOptions);
|
||||
});
|
||||
const columns: AdvancedTableColumn<User>[] = [
|
||||
{ label: 'ID', column: 'id', hidden: true },
|
||||
{ label: m.profile_picture(), key: 'profilePicture', cell: ProfilePictureCell },
|
||||
{ label: m.first_name(), column: 'firstName', sortable: true, hidden: true },
|
||||
{ label: m.last_name(), column: 'lastName', sortable: true, hidden: true },
|
||||
{ label: m.display_name(), column: 'displayName', sortable: true },
|
||||
{ label: m.email(), column: 'email', sortable: true, hidden: true },
|
||||
{ label: m.username(), column: 'username', sortable: true },
|
||||
{
|
||||
label: m.role(),
|
||||
column: 'isAdmin',
|
||||
sortable: true,
|
||||
filterableValues: [
|
||||
{ label: m.admin(), value: true },
|
||||
{ label: m.user(), value: false }
|
||||
],
|
||||
value: (item) => (item.isAdmin ? m.admin() : m.user()),
|
||||
hidden: true
|
||||
},
|
||||
{
|
||||
label: m.status(),
|
||||
column: 'disabled',
|
||||
cell: StatusCell,
|
||||
sortable: true,
|
||||
filterableValues: [
|
||||
{
|
||||
label: m.enabled(),
|
||||
value: false
|
||||
},
|
||||
{
|
||||
label: m.disabled(),
|
||||
value: true
|
||||
}
|
||||
]
|
||||
},
|
||||
{ label: m.ldap_id(), column: 'ldapId', hidden: true },
|
||||
{ label: m.locale(), column: 'locale', hidden: true }
|
||||
];
|
||||
</script>
|
||||
|
||||
{#if users}
|
||||
<AdvancedTable
|
||||
items={users}
|
||||
onRefresh={async (o) => (users = await userService.list(o))}
|
||||
{requestOptions}
|
||||
columns={[
|
||||
{ label: m.name(), sortColumn: 'firstName' },
|
||||
{ label: m.email(), sortColumn: 'email' }
|
||||
]}
|
||||
bind:selectedIds={selectedUserIds}
|
||||
{selectionDisabled}
|
||||
>
|
||||
{#snippet rows({ item })}
|
||||
<Table.Cell>{item.displayName}</Table.Cell>
|
||||
<Table.Cell>{item.email}</Table.Cell>
|
||||
{/snippet}
|
||||
</AdvancedTable>
|
||||
{/if}
|
||||
{#snippet ProfilePictureCell({ item }: { item: User })}
|
||||
<Avatar.Root class="size-8">
|
||||
<Avatar.Image class="object-cover" src={cachedProfilePicture.getUrl(item.id)} />
|
||||
</Avatar.Root>
|
||||
{/snippet}
|
||||
|
||||
{#snippet StatusCell({ item }: { item: User })}
|
||||
<Badge class="rounded-full" variant={item.disabled ? 'destructive' : 'default'}>
|
||||
{item.disabled ? m.disabled() : m.enabled()}
|
||||
</Badge>
|
||||
{/snippet}
|
||||
|
||||
<AdvancedTable
|
||||
id="user-selection"
|
||||
fetchCallback={userService.list}
|
||||
defaultSort={{ column: 'firstName', direction: 'asc' }}
|
||||
bind:selectedIds={selectedUserIds}
|
||||
{selectionDisabled}
|
||||
{columns}
|
||||
/>
|
||||
|
||||
@@ -15,17 +15,12 @@
|
||||
import UserForm from './user-form.svelte';
|
||||
import UserList from './user-list.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
let users = $state(data.users);
|
||||
let usersRequestOptions = $state(data.usersRequestOptions);
|
||||
let signupTokens = $state(data.signupTokens);
|
||||
let signupTokensRequestOptions = $state(data.signupTokensRequestOptions);
|
||||
|
||||
let selectedCreateOptions = $state(m.add_user());
|
||||
let expandAddUser = $state(false);
|
||||
let signupTokenModalOpen = $state(false);
|
||||
let signupTokenListModalOpen = $state(false);
|
||||
|
||||
let userListRef: UserList;
|
||||
const userService = new UserService();
|
||||
|
||||
async function createUser(user: UserCreate) {
|
||||
@@ -38,13 +33,9 @@
|
||||
success = false;
|
||||
});
|
||||
|
||||
users = await userService.list(usersRequestOptions);
|
||||
await userListRef.refresh();
|
||||
return success;
|
||||
}
|
||||
|
||||
async function refreshSignupTokens() {
|
||||
signupTokens = await userService.listSignupTokens(signupTokensRequestOptions);
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -117,15 +108,10 @@
|
||||
</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<UserList {users} requestOptions={usersRequestOptions} />
|
||||
<UserList bind:this={userListRef} />
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</div>
|
||||
|
||||
<SignupTokenModal bind:open={signupTokenModalOpen} onTokenCreated={refreshSignupTokens} />
|
||||
<SignupTokenListModal
|
||||
bind:open={signupTokenListModalOpen}
|
||||
bind:signupTokens
|
||||
{signupTokensRequestOptions}
|
||||
onTokenDeleted={refreshSignupTokens}
|
||||
/>
|
||||
<SignupTokenModal bind:open={signupTokenModalOpen} />
|
||||
<SignupTokenListModal bind:open={signupTokenListModalOpen} />
|
||||
|
||||
@@ -1,33 +0,0 @@
|
||||
import UserService from '$lib/services/user-service';
|
||||
import type { SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async () => {
|
||||
const userService = new UserService();
|
||||
|
||||
const usersRequestOptions: SearchPaginationSortRequest = {
|
||||
sort: {
|
||||
column: 'firstName',
|
||||
direction: 'asc'
|
||||
}
|
||||
};
|
||||
|
||||
const signupTokensRequestOptions: SearchPaginationSortRequest = {
|
||||
sort: {
|
||||
column: 'createdAt',
|
||||
direction: 'desc'
|
||||
}
|
||||
};
|
||||
|
||||
const [users, signupTokens] = await Promise.all([
|
||||
userService.list(usersRequestOptions),
|
||||
userService.listSignupTokens(signupTokensRequestOptions)
|
||||
]);
|
||||
|
||||
return {
|
||||
users,
|
||||
usersRequestOptions,
|
||||
signupTokens,
|
||||
signupTokensRequestOptions
|
||||
};
|
||||
};
|
||||
@@ -14,6 +14,7 @@
|
||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||
import { LucideChevronLeft } from '@lucide/svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { backNavigate } from '../navigate-back-util';
|
||||
import UserForm from '../user-form.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
@@ -24,6 +25,7 @@
|
||||
|
||||
const userService = new UserService();
|
||||
const customClaimService = new CustomClaimService();
|
||||
const backNavigation = backNavigate('/settings/admin/users');
|
||||
|
||||
async function updateUserGroups(userIds: string[]) {
|
||||
await userService
|
||||
@@ -81,8 +83,8 @@
|
||||
</svelte:head>
|
||||
|
||||
<div class="flex items-center justify-between">
|
||||
<a class="text-muted-foreground flex text-sm" href="/settings/admin/users"
|
||||
><LucideChevronLeft class="size-5" /> {m.back()}</a
|
||||
<button class="text-muted-foreground flex text-sm" onclick={() => backNavigation.go()}
|
||||
><LucideChevronLeft class="size-5" /> {m.back()}</button
|
||||
>
|
||||
{#if !!user.ldapId}
|
||||
<Badge class="rounded-full" variant="default">{m.ldap()}</Badge>
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { afterNavigate, goto } from '$app/navigation';
|
||||
|
||||
export const backNavigate = (defaultRoute: string) => {
|
||||
let previousUrl: URL | undefined;
|
||||
afterNavigate((e) => {
|
||||
if (e.from) {
|
||||
previousUrl = e.from.url;
|
||||
}
|
||||
});
|
||||
|
||||
return {
|
||||
go: () => {
|
||||
if (previousUrl && previousUrl.pathname === defaultRoute) {
|
||||
window.history.back();
|
||||
} else {
|
||||
goto(defaultRoute);
|
||||
}
|
||||
}
|
||||
};
|
||||
};
|
||||
@@ -1,18 +1,20 @@
|
||||
<script lang="ts">
|
||||
import { goto } from '$app/navigation';
|
||||
import AdvancedTable from '$lib/components/advanced-table.svelte';
|
||||
import { openConfirmDialog } from '$lib/components/confirm-dialog/';
|
||||
import OneTimeLinkModal from '$lib/components/one-time-link-modal.svelte';
|
||||
import AdvancedTable from '$lib/components/table/advanced-table.svelte';
|
||||
import * as Avatar from '$lib/components/ui/avatar/index';
|
||||
import { Badge } from '$lib/components/ui/badge/index';
|
||||
import { buttonVariants } from '$lib/components/ui/button';
|
||||
import * as DropdownMenu from '$lib/components/ui/dropdown-menu';
|
||||
import * as Table from '$lib/components/ui/table';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import UserService from '$lib/services/user-service';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
import userStore from '$lib/stores/user-store';
|
||||
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||
import type {
|
||||
AdvancedTableColumn,
|
||||
CreateAdvancedTableActions
|
||||
} from '$lib/types/advanced-table.type';
|
||||
import type { User } from '$lib/types/user.type';
|
||||
import { cachedProfilePicture } from '$lib/utils/cached-image-util';
|
||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||
import {
|
||||
LucideLink,
|
||||
@@ -21,18 +23,17 @@
|
||||
LucideUserCheck,
|
||||
LucideUserX
|
||||
} from '@lucide/svelte';
|
||||
import Ellipsis from '@lucide/svelte/icons/ellipsis';
|
||||
import { toast } from 'svelte-sonner';
|
||||
|
||||
let {
|
||||
users = $bindable(),
|
||||
requestOptions
|
||||
}: { users: Paginated<User>; requestOptions: SearchPaginationSortRequest } = $props();
|
||||
|
||||
let userIdToCreateOneTimeLink: string | null = $state(null);
|
||||
let tableRef: AdvancedTable<User>;
|
||||
|
||||
const userService = new UserService();
|
||||
|
||||
export function refresh() {
|
||||
return tableRef?.refresh();
|
||||
}
|
||||
|
||||
async function deleteUser(user: User) {
|
||||
openConfirmDialog({
|
||||
title: m.delete_firstname_lastname({
|
||||
@@ -46,7 +47,7 @@
|
||||
action: async () => {
|
||||
try {
|
||||
await userService.remove(user.id);
|
||||
users = await userService.list(requestOptions!);
|
||||
await refresh();
|
||||
} catch (e) {
|
||||
axiosErrorToast(e);
|
||||
}
|
||||
@@ -62,9 +63,9 @@
|
||||
...user,
|
||||
disabled: false
|
||||
})
|
||||
.then(() => {
|
||||
.then(async () => {
|
||||
toast.success(m.user_enabled_successfully());
|
||||
userService.list(requestOptions!).then((updatedUsers) => (users = updatedUsers));
|
||||
await refresh();
|
||||
})
|
||||
.catch(axiosErrorToast);
|
||||
}
|
||||
@@ -85,7 +86,7 @@
|
||||
...user,
|
||||
disabled: true
|
||||
});
|
||||
users = await userService.list(requestOptions!);
|
||||
await refresh();
|
||||
toast.success(m.user_disabled_successfully());
|
||||
} catch (e) {
|
||||
axiosErrorToast(e);
|
||||
@@ -94,85 +95,99 @@
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
const columns: AdvancedTableColumn<User>[] = [
|
||||
{ label: 'ID', column: 'id', hidden: true },
|
||||
{ label: m.profile_picture(), key: 'profilePicture', cell: ProfilePictureCell },
|
||||
{ label: m.first_name(), column: 'firstName', sortable: true },
|
||||
{ label: m.last_name(), column: 'lastName', sortable: true },
|
||||
{ label: m.display_name(), column: 'displayName', sortable: true },
|
||||
{ label: m.email(), column: 'email', sortable: true },
|
||||
{ label: m.username(), column: 'username', sortable: true },
|
||||
{
|
||||
label: m.role(),
|
||||
column: 'isAdmin',
|
||||
sortable: true,
|
||||
filterableValues: [
|
||||
{ label: m.admin(), value: true },
|
||||
{ label: m.user(), value: false }
|
||||
],
|
||||
value: (item) => (item.isAdmin ? m.admin() : m.user())
|
||||
},
|
||||
{
|
||||
label: m.status(),
|
||||
column: 'disabled',
|
||||
cell: StatusCell,
|
||||
sortable: true,
|
||||
filterableValues: [
|
||||
{
|
||||
label: m.enabled(),
|
||||
value: false
|
||||
},
|
||||
{
|
||||
label: m.disabled(),
|
||||
value: true
|
||||
}
|
||||
]
|
||||
},
|
||||
{ label: m.ldap_id(), column: 'ldapId', hidden: true },
|
||||
{ label: m.locale(), column: 'locale', hidden: true },
|
||||
{ label: m.source(), key: 'source', hidden: !$appConfigStore.ldapEnabled, cell: SourceCell }
|
||||
];
|
||||
|
||||
const actions: CreateAdvancedTableActions<User> = (u) => [
|
||||
{
|
||||
label: m.login_code(),
|
||||
icon: LucideLink,
|
||||
onClick: (u) => (userIdToCreateOneTimeLink = u.id)
|
||||
},
|
||||
{
|
||||
label: m.edit(),
|
||||
icon: LucidePencil,
|
||||
onClick: (u) => goto(`/settings/admin/users/${u.id}`)
|
||||
},
|
||||
{
|
||||
label: u.disabled ? m.enable() : m.disable(),
|
||||
icon: u.disabled ? LucideUserCheck : LucideUserX,
|
||||
onClick: (u) => (u.disabled ? enableUser(u) : disableUser(u)),
|
||||
hidden: !!u.ldapId || $appConfigStore.ldapEnabled,
|
||||
disabled: u.id === $userStore?.id
|
||||
},
|
||||
{
|
||||
label: m.delete(),
|
||||
icon: LucideTrash,
|
||||
variant: 'danger',
|
||||
onClick: (u) => deleteUser(u),
|
||||
hidden: !!u.ldapId && !u.disabled,
|
||||
disabled: u.id === $userStore?.id
|
||||
}
|
||||
];
|
||||
</script>
|
||||
|
||||
{#snippet ProfilePictureCell({ item }: { item: User })}
|
||||
<Avatar.Root class="size-8">
|
||||
<Avatar.Image class="object-cover" src={cachedProfilePicture.getUrl(item.id)} />
|
||||
</Avatar.Root>
|
||||
{/snippet}
|
||||
|
||||
{#snippet StatusCell({ item }: { item: User })}
|
||||
<Badge class="rounded-full" variant={item.disabled ? 'destructive' : 'default'}>
|
||||
{item.disabled ? m.disabled() : m.enabled()}
|
||||
</Badge>
|
||||
{/snippet}
|
||||
|
||||
{#snippet SourceCell({ item }: { item: User })}
|
||||
<Badge class="rounded-full" variant={item.ldapId ? 'default' : 'outline'}>
|
||||
{item.ldapId ? m.ldap() : m.local()}
|
||||
</Badge>
|
||||
{/snippet}
|
||||
|
||||
<AdvancedTable
|
||||
items={users}
|
||||
{requestOptions}
|
||||
onRefresh={async (options) => (users = await userService.list(options))}
|
||||
columns={[
|
||||
{ label: m.first_name(), sortColumn: 'firstName' },
|
||||
{ label: m.last_name(), sortColumn: 'lastName' },
|
||||
{ label: m.display_name(), sortColumn: 'displayName' },
|
||||
{ label: m.email(), sortColumn: 'email' },
|
||||
{ label: m.username(), sortColumn: 'username' },
|
||||
{ label: m.role(), sortColumn: 'isAdmin' },
|
||||
{ label: m.status(), sortColumn: 'disabled' },
|
||||
...($appConfigStore.ldapEnabled ? [{ label: m.source() }] : []),
|
||||
{ label: m.actions(), hidden: true }
|
||||
]}
|
||||
>
|
||||
{#snippet rows({ item })}
|
||||
<Table.Cell>{item.firstName}</Table.Cell>
|
||||
<Table.Cell>{item.lastName}</Table.Cell>
|
||||
<Table.Cell>{item.displayName}</Table.Cell>
|
||||
<Table.Cell>{item.email}</Table.Cell>
|
||||
<Table.Cell>{item.username}</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Badge class="rounded-full" variant="outline">{item.isAdmin ? m.admin() : m.user()}</Badge>
|
||||
</Table.Cell>
|
||||
<Table.Cell>
|
||||
<Badge class="rounded-full" variant={item.disabled ? 'destructive' : 'default'}>
|
||||
{item.disabled ? m.disabled() : m.enabled()}
|
||||
</Badge>
|
||||
</Table.Cell>
|
||||
{#if $appConfigStore.ldapEnabled}
|
||||
<Table.Cell>
|
||||
<Badge class="rounded-full" variant={item.ldapId ? 'default' : 'outline'}
|
||||
>{item.ldapId ? m.ldap() : m.local()}</Badge
|
||||
>
|
||||
</Table.Cell>
|
||||
{/if}
|
||||
<Table.Cell>
|
||||
<DropdownMenu.Root>
|
||||
<DropdownMenu.Trigger class={buttonVariants({ variant: 'ghost', size: 'icon' })}>
|
||||
<Ellipsis class="size-4" />
|
||||
<span class="sr-only">{m.toggle_menu()}</span>
|
||||
</DropdownMenu.Trigger>
|
||||
<DropdownMenu.Content align="end">
|
||||
<DropdownMenu.Item onclick={() => (userIdToCreateOneTimeLink = item.id)}
|
||||
><LucideLink class="mr-2 size-4" />{m.login_code()}</DropdownMenu.Item
|
||||
>
|
||||
<DropdownMenu.Item onclick={() => goto(`/settings/admin/users/${item.id}`)}
|
||||
><LucidePencil class="mr-2 size-4" /> {m.edit()}</DropdownMenu.Item
|
||||
>
|
||||
{#if !item.ldapId || !$appConfigStore.ldapEnabled}
|
||||
{#if item.disabled}
|
||||
<DropdownMenu.Item
|
||||
disabled={item.id === $userStore?.id}
|
||||
onclick={() => enableUser(item)}
|
||||
><LucideUserCheck class="mr-2 size-4" />{m.enable()}</DropdownMenu.Item
|
||||
>
|
||||
{:else}
|
||||
<DropdownMenu.Item
|
||||
disabled={item.id === $userStore?.id}
|
||||
onclick={() => disableUser(item)}
|
||||
><LucideUserX class="mr-2 size-4" />{m.disable()}</DropdownMenu.Item
|
||||
>
|
||||
{/if}
|
||||
{/if}
|
||||
{#if !item.ldapId || (item.ldapId && item.disabled)}
|
||||
<DropdownMenu.Item
|
||||
class="text-red-500 focus:!text-red-700"
|
||||
disabled={item.id === $userStore?.id}
|
||||
onclick={() => deleteUser(item)}
|
||||
><LucideTrash class="mr-2 size-4" />{m.delete()}</DropdownMenu.Item
|
||||
>
|
||||
{/if}
|
||||
</DropdownMenu.Content>
|
||||
</DropdownMenu.Root>
|
||||
</Table.Cell>
|
||||
{/snippet}
|
||||
</AdvancedTable>
|
||||
id="user-list"
|
||||
bind:this={tableRef}
|
||||
fetchCallback={userService.list}
|
||||
{actions}
|
||||
{columns}
|
||||
/>
|
||||
|
||||
<OneTimeLinkModal bind:userId={userIdToCreateOneTimeLink} />
|
||||
|
||||
@@ -3,8 +3,8 @@
|
||||
import * as Pagination from '$lib/components/ui/pagination';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import OIDCService from '$lib/services/oidc-service';
|
||||
import type { ListRequestOptions, Paginated } from '$lib/types/list-request.type';
|
||||
import type { AccessibleOidcClient, OidcClientMetaData } from '$lib/types/oidc.type';
|
||||
import type { Paginated, SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||
import { LayoutDashboard } from '@lucide/svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
@@ -12,11 +12,11 @@
|
||||
|
||||
let { data } = $props();
|
||||
let clients: Paginated<AccessibleOidcClient> = $state(data.clients);
|
||||
let requestOptions: SearchPaginationSortRequest = $state(data.appRequestOptions);
|
||||
let requestOptions: ListRequestOptions = $state(data.appRequestOptions);
|
||||
|
||||
const oidcService = new OIDCService();
|
||||
|
||||
async function onRefresh(options: SearchPaginationSortRequest) {
|
||||
async function onRefresh(options: ListRequestOptions) {
|
||||
clients = await oidcService.listOwnAccessibleClients(options);
|
||||
}
|
||||
|
||||
@@ -83,6 +83,10 @@
|
||||
{#each clients.data as client}
|
||||
<AuthorizedOidcClientCard {client} onRevoke={revokeAuthorizedClient} />
|
||||
{/each}
|
||||
<!-- Gap fix if two elements are present-->
|
||||
{#if clients.data.length == 2}
|
||||
<div></div>
|
||||
{/if}
|
||||
</div>
|
||||
|
||||
{#if clients.pagination.totalPages > 1}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
import OIDCService from '$lib/services/oidc-service';
|
||||
import type { SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||
import type { ListRequestOptions } from '$lib/types/list-request.type';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async () => {
|
||||
const oidcService = new OIDCService();
|
||||
|
||||
const appRequestOptions: SearchPaginationSortRequest = {
|
||||
const appRequestOptions: ListRequestOptions = {
|
||||
pagination: {
|
||||
page: 1,
|
||||
limit: 20
|
||||
|
||||
@@ -38,7 +38,7 @@
|
||||
<div class="flex gap-3">
|
||||
<div class="aspect-square h-[56px]">
|
||||
<ImageBox
|
||||
class="size-8"
|
||||
class="size-14"
|
||||
src={client.hasLogo
|
||||
? cachedOidcClientLogo.getUrl(client.id)
|
||||
: cachedApplicationLogo.getUrl(isLightMode)}
|
||||
|
||||
@@ -5,9 +5,6 @@
|
||||
import userStore from '$lib/stores/user-store';
|
||||
import { LogsIcon } from '@lucide/svelte';
|
||||
import AuditLogSwitcher from './audit-log-switcher.svelte';
|
||||
|
||||
let { data } = $props();
|
||||
let auditLogsRequestOptions = $state(data.auditLogsRequestOptions);
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -19,7 +16,7 @@
|
||||
{/if}
|
||||
|
||||
<div>
|
||||
<Card.Root>
|
||||
<Card.Root class="gap-0">
|
||||
<Card.Header>
|
||||
<Card.Title>
|
||||
<LogsIcon class="text-primary/80 size-5" />
|
||||
@@ -28,7 +25,7 @@
|
||||
<Card.Description>{m.see_your_account_activities_from_the_last_3_months()}</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<AuditLogList auditLogs={data.auditLogs} requestOptions={auditLogsRequestOptions} />
|
||||
<AuditLogList />
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</div>
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
import AuditLogService from '$lib/services/audit-log-service';
|
||||
import type { SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async () => {
|
||||
const auditLogService = new AuditLogService();
|
||||
const auditLogsRequestOptions: SearchPaginationSortRequest = {
|
||||
sort: {
|
||||
column: 'createdAt',
|
||||
direction: 'desc'
|
||||
}
|
||||
};
|
||||
const auditLogs = await auditLogService.list(auditLogsRequestOptions);
|
||||
return { auditLogs, auditLogsRequestOptions };
|
||||
};
|
||||
@@ -6,15 +6,11 @@
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import AuditLogService from '$lib/services/audit-log-service';
|
||||
import type { AuditLogFilter } from '$lib/types/audit-log.type';
|
||||
import { eventTypes as eventTranslations } from '$lib/utils/audit-log-translator';
|
||||
import AuditLogSwitcher from '../audit-log-switcher.svelte';
|
||||
import {eventTypes as eventTranslations} from "$lib/utils/audit-log-translator";
|
||||
|
||||
let { data } = $props();
|
||||
|
||||
const auditLogService = new AuditLogService();
|
||||
|
||||
let auditLogs = $state(data.auditLogs);
|
||||
let requestOptions = $state(data.requestOptions);
|
||||
let auditLogListRef: AuditLogList;
|
||||
|
||||
let filters: AuditLogFilter = $state({
|
||||
userId: '',
|
||||
@@ -29,10 +25,6 @@
|
||||
});
|
||||
|
||||
const eventTypes = $state(eventTranslations);
|
||||
|
||||
$effect(() => {
|
||||
auditLogService.listAllLogs(requestOptions, filters).then((response) => (auditLogs = response));
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:head>
|
||||
@@ -124,7 +116,6 @@
|
||||
{/await}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<AuditLogList isAdmin={true} {auditLogs} {requestOptions} />
|
||||
<AuditLogList bind:this={auditLogListRef} isAdmin {filters} />
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
@@ -1,21 +0,0 @@
|
||||
import AuditLogService from '$lib/services/audit-log-service';
|
||||
import type { SearchPaginationSortRequest } from '$lib/types/pagination.type';
|
||||
import type { PageLoad } from './$types';
|
||||
|
||||
export const load: PageLoad = async () => {
|
||||
const auditLogService = new AuditLogService();
|
||||
|
||||
const requestOptions: SearchPaginationSortRequest = {
|
||||
sort: {
|
||||
column: 'createdAt',
|
||||
direction: 'desc'
|
||||
}
|
||||
};
|
||||
|
||||
const auditLogs = await auditLogService.listAllLogs(requestOptions);
|
||||
|
||||
return {
|
||||
auditLogs,
|
||||
requestOptions
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user