mirror of
https://github.com/pocket-id/pocket-id.git
synced 2026-07-16 04:03:47 +03:00
feat: add tab bar navigation for crowded pages
This commit is contained in:
@@ -28,7 +28,7 @@
|
||||
>
|
||||
<div
|
||||
class="{!isAuthPage
|
||||
? 'max-w-410'
|
||||
? 'max-w-[1720px]'
|
||||
: ''} mx-auto flex w-full items-center justify-between px-4 md:px-10"
|
||||
>
|
||||
<div class="flex h-16 items-center">
|
||||
|
||||
@@ -12,6 +12,9 @@
|
||||
<TabsPrimitive.Content
|
||||
bind:ref
|
||||
data-slot="tabs-content"
|
||||
class={cn('text-sm flex-1 outline-none', className)}
|
||||
class={cn(
|
||||
'text-sm flex-1 outline-none data-[state=active]:animate-in data-[state=active]:fade-in-0 data-[state=active]:slide-in-from-bottom-1 data-[state=active]:duration-200 motion-reduce:data-[state=active]:animate-none',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
/>
|
||||
|
||||
@@ -19,22 +19,105 @@
|
||||
|
||||
<script lang="ts">
|
||||
import { Tabs as TabsPrimitive } from 'bits-ui';
|
||||
import { onMount } from 'svelte';
|
||||
import { cn } from '$lib/utils/style.js';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
variant = 'default',
|
||||
class: className,
|
||||
children,
|
||||
...restProps
|
||||
}: TabsPrimitive.ListProps & {
|
||||
variant?: TabsListVariant;
|
||||
} = $props();
|
||||
|
||||
let indicatorOffset = $state(0);
|
||||
let indicatorSize = $state(0);
|
||||
let indicatorVisible = $state(false);
|
||||
let indicatorOrientation = $state<'horizontal' | 'vertical'>('horizontal');
|
||||
|
||||
function updateIndicator() {
|
||||
if (!ref || variant !== 'line') {
|
||||
indicatorVisible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const activeTrigger = ref.querySelector<HTMLElement>(
|
||||
'[data-slot="tabs-trigger"][data-state="active"]'
|
||||
);
|
||||
if (!activeTrigger) {
|
||||
indicatorVisible = false;
|
||||
return;
|
||||
}
|
||||
|
||||
const listRect = ref.getBoundingClientRect();
|
||||
const triggerRect = activeTrigger.getBoundingClientRect();
|
||||
indicatorOrientation = ref.dataset.orientation === 'vertical' ? 'vertical' : 'horizontal';
|
||||
|
||||
if (indicatorOrientation === 'vertical') {
|
||||
indicatorOffset = triggerRect.top - listRect.top;
|
||||
indicatorSize = triggerRect.height;
|
||||
} else {
|
||||
const triggerStyle = getComputedStyle(activeTrigger);
|
||||
const paddingLeft = Number.parseFloat(triggerStyle.paddingLeft);
|
||||
const paddingRight = Number.parseFloat(triggerStyle.paddingRight);
|
||||
indicatorOffset = triggerRect.left - listRect.left + paddingLeft;
|
||||
indicatorSize = triggerRect.width - paddingLeft - paddingRight;
|
||||
}
|
||||
indicatorVisible = true;
|
||||
}
|
||||
|
||||
onMount(() => {
|
||||
if (!ref || variant !== 'line') return;
|
||||
|
||||
const resizeObserver = new ResizeObserver(updateIndicator);
|
||||
const observeTriggers = () => {
|
||||
ref
|
||||
?.querySelectorAll<HTMLElement>('[data-slot="tabs-trigger"]')
|
||||
.forEach((trigger) => resizeObserver.observe(trigger));
|
||||
updateIndicator();
|
||||
};
|
||||
const mutationObserver = new MutationObserver(observeTriggers);
|
||||
|
||||
resizeObserver.observe(ref);
|
||||
observeTriggers();
|
||||
mutationObserver.observe(ref, {
|
||||
attributes: true,
|
||||
attributeFilter: ['data-state'],
|
||||
childList: true,
|
||||
subtree: true
|
||||
});
|
||||
|
||||
return () => {
|
||||
mutationObserver.disconnect();
|
||||
resizeObserver.disconnect();
|
||||
};
|
||||
});
|
||||
</script>
|
||||
|
||||
<TabsPrimitive.List
|
||||
bind:ref
|
||||
data-slot="tabs-list"
|
||||
data-variant={variant}
|
||||
class={cn(tabsListVariants({ variant }), className)}
|
||||
class={cn(tabsListVariants({ variant }), variant === 'line' && 'relative', className)}
|
||||
{...restProps}
|
||||
/>
|
||||
>
|
||||
{@render children?.()}
|
||||
{#if variant === 'line'}
|
||||
<span
|
||||
aria-hidden="true"
|
||||
data-slot="tabs-indicator"
|
||||
class={cn(
|
||||
'bg-foreground pointer-events-none absolute rounded-full opacity-0 transition-[transform,width,height,opacity] duration-300 ease-out motion-reduce:transition-none',
|
||||
indicatorOrientation === 'horizontal' ? 'bottom-0 left-0 h-0.5' : 'top-0 right-0 w-0.5',
|
||||
indicatorVisible && 'opacity-100'
|
||||
)}
|
||||
style:width={indicatorOrientation === 'horizontal' ? `${indicatorSize}px` : undefined}
|
||||
style:height={indicatorOrientation === 'vertical' ? `${indicatorSize}px` : undefined}
|
||||
style:transform={indicatorOrientation === 'horizontal'
|
||||
? `translate3d(${indicatorOffset}px, 0, 0)`
|
||||
: `translate3d(0, ${indicatorOffset}px, 0)`}
|
||||
></span>
|
||||
{/if}
|
||||
</TabsPrimitive.List>
|
||||
|
||||
@@ -16,7 +16,6 @@
|
||||
"gap-2 rounded-full border border-transparent! px-3 py-1 text-sm font-medium group-data-vertical/tabs:rounded-2xl group-data-vertical/tabs:px-3 group-data-vertical/tabs:py-1.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2 [&_svg:not([class*='size-'])]:size-4 focus-visible:border-ring focus-visible:ring-ring/50 focus-visible:outline-ring text-foreground/60 hover:text-foreground dark:text-muted-foreground dark:hover:text-foreground relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center whitespace-nowrap transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start focus-visible:ring-[3px] focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0",
|
||||
'group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-active:bg-transparent dark:group-data-[variant=line]/tabs-list:data-active:border-transparent dark:group-data-[variant=line]/tabs-list:data-active:bg-transparent',
|
||||
'data-active:bg-background dark:data-active:text-foreground dark:data-active:border-input dark:data-active:bg-input/30 data-active:text-foreground',
|
||||
'after:bg-foreground after:absolute after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-active:after:opacity-100',
|
||||
className
|
||||
)}
|
||||
{...restProps}
|
||||
|
||||
@@ -1,18 +1,36 @@
|
||||
<script lang="ts">
|
||||
import { Tabs as TabsPrimitive } from 'bits-ui';
|
||||
import { page } from '$app/state';
|
||||
import { cn } from '$lib/utils/style.js';
|
||||
import { Tabs as TabsPrimitive } from 'bits-ui';
|
||||
import { onMount } from 'svelte';
|
||||
|
||||
let {
|
||||
ref = $bindable(null),
|
||||
value = $bindable(''),
|
||||
useHash = false,
|
||||
class: className,
|
||||
...restProps
|
||||
}: TabsPrimitive.RootProps = $props();
|
||||
}: TabsPrimitive.RootProps & {
|
||||
useHash?: boolean;
|
||||
} = $props();
|
||||
|
||||
onMount(() => {
|
||||
if (useHash && page.url.hash) {
|
||||
value = page.url.hash.substring(1);
|
||||
}
|
||||
});
|
||||
|
||||
function onTabChange(newValue: string) {
|
||||
if (useHash && page.url.hash !== newValue) {
|
||||
window.location.hash = newValue;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<TabsPrimitive.Root
|
||||
bind:ref
|
||||
bind:value
|
||||
onValueChange={onTabChange}
|
||||
data-slot="tabs"
|
||||
class={cn('gap-2 group/tabs flex data-[orientation=horizontal]:flex-col', className)}
|
||||
{...restProps}
|
||||
|
||||
@@ -50,7 +50,7 @@
|
||||
>
|
||||
<main
|
||||
in:fade={{ duration: 200 }}
|
||||
class="mx-auto flex w-full max-w-[1640px] flex-col gap-x-8 gap-y-8 p-4 md:p-8 lg:flex-row"
|
||||
class="mx-auto flex w-full max-w-[1720px] flex-col gap-x-8 gap-y-8 p-4 md:p-8 lg:flex-row"
|
||||
>
|
||||
<div class="min-w-[200px] xl:min-w-[250px]">
|
||||
<div in:fly={{ x: -15, duration: 200 }} class="sticky top-6">
|
||||
|
||||
@@ -1,18 +1,14 @@
|
||||
<script lang="ts">
|
||||
import CollapsibleCard from '$lib/components/collapsible-card.svelte';
|
||||
import * as Alert from '$lib/components/ui/alert';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import * as Tabs from '$lib/components/ui/tabs';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import AppConfigService from '$lib/services/app-config-service';
|
||||
import appConfigStore from '$lib/stores/application-configuration-store';
|
||||
import type { AllAppConfig } from '$lib/types/application-configuration.type';
|
||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||
import {
|
||||
LucideImage,
|
||||
LucideInfo,
|
||||
Mail,
|
||||
SlidersHorizontal,
|
||||
UserSearch,
|
||||
Users
|
||||
LucideInfo
|
||||
} from '@lucide/svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import AppConfigEmailForm from './forms/app-config-email-form.svelte';
|
||||
@@ -101,57 +97,85 @@
|
||||
</Alert.Description>
|
||||
</Alert.Root>
|
||||
{/if}
|
||||
<div>
|
||||
<CollapsibleCard
|
||||
id="application-configuration-general"
|
||||
icon={SlidersHorizontal}
|
||||
title={m.general()}
|
||||
defaultExpanded
|
||||
>
|
||||
<AppConfigGeneralForm {appConfig} callback={updateAppConfig} />
|
||||
</CollapsibleCard>
|
||||
</div>
|
||||
<Tabs.Root value="general" useHash class="gap-4">
|
||||
<div class="overflow-x-auto pb-1">
|
||||
<Tabs.List variant="line" class="min-w-max">
|
||||
<Tabs.Trigger value="general">
|
||||
{m.general()}
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="user-creation">
|
||||
{m.user_creation()}
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="email">
|
||||
{m.email()}
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="ldap">
|
||||
{m.ldap()}
|
||||
</Tabs.Trigger>
|
||||
<Tabs.Trigger value="images">
|
||||
{m.images()}
|
||||
</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<CollapsibleCard
|
||||
id="application-configuration-signup-defaults"
|
||||
icon={Users}
|
||||
title={m.user_creation()}
|
||||
description={m.configure_user_creation()}
|
||||
>
|
||||
<AppConfigSignupDefaultsForm {appConfig} callback={updateAppConfig} />
|
||||
</CollapsibleCard>
|
||||
</div>
|
||||
<Tabs.Content value="general" id="application-configuration-general">
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>{m.general()}</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<AppConfigGeneralForm {appConfig} callback={updateAppConfig} />
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</Tabs.Content>
|
||||
|
||||
<div>
|
||||
<CollapsibleCard
|
||||
id="application-configuration-email"
|
||||
icon={Mail}
|
||||
title={m.email()}
|
||||
description={m.configure_smtp_to_send_emails()}
|
||||
>
|
||||
<AppConfigEmailForm {appConfig} callback={updateAppConfig} />
|
||||
</CollapsibleCard>
|
||||
</div>
|
||||
<Tabs.Content value="user-creation" id="application-configuration-signup-defaults">
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>{m.user_creation()}</Card.Title>
|
||||
<Card.Description>{m.configure_user_creation()}</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<AppConfigSignupDefaultsForm {appConfig} callback={updateAppConfig} />
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</Tabs.Content>
|
||||
|
||||
<div>
|
||||
<CollapsibleCard
|
||||
id="application-configuration-ldap"
|
||||
icon={UserSearch}
|
||||
title={m.ldap()}
|
||||
description={m.configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server()}
|
||||
>
|
||||
<AppConfigLdapForm {appConfig} callback={updateAppConfig} />
|
||||
</CollapsibleCard>
|
||||
</div>
|
||||
<Tabs.Content value="email" id="application-configuration-email">
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>{m.email()}</Card.Title>
|
||||
<Card.Description>{m.configure_smtp_to_send_emails()}</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<AppConfigEmailForm {appConfig} callback={updateAppConfig} />
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</Tabs.Content>
|
||||
|
||||
<div>
|
||||
<CollapsibleCard
|
||||
id="application-configuration-images"
|
||||
icon={LucideImage}
|
||||
title={m.images()}
|
||||
description={m.configure_application_images()}
|
||||
>
|
||||
<UpdateApplicationImages callback={updateImages} />
|
||||
</CollapsibleCard>
|
||||
</div>
|
||||
<Tabs.Content value="ldap" id="application-configuration-ldap">
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>{m.ldap()}</Card.Title>
|
||||
<Card.Description>
|
||||
{m.configure_ldap_settings_to_sync_users_and_groups_from_an_ldap_server()}
|
||||
</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<AppConfigLdapForm {appConfig} callback={updateAppConfig} />
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="images" id="application-configuration-images">
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>{m.images()}</Card.Title>
|
||||
<Card.Description>{m.configure_application_images()}</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<UpdateApplicationImages callback={updateImages} />
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
|
||||
@@ -1,13 +1,14 @@
|
||||
<script lang="ts">
|
||||
import { beforeNavigate } from '$app/navigation';
|
||||
import { page } from '$app/state';
|
||||
import CollapsibleCard from '$lib/components/collapsible-card.svelte';
|
||||
import { openConfirmDialog } from '$lib/components/confirm-dialog';
|
||||
import CopyToClipboard from '$lib/components/copy-to-clipboard.svelte';
|
||||
import FormattedMessage from '$lib/components/formatted-message.svelte';
|
||||
import * as Alert from '$lib/components/ui/alert';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import * as Field from '$lib/components/ui/field';
|
||||
import * as Tabs from '$lib/components/ui/tabs';
|
||||
import UserGroupSelection from '$lib/components/user-group-selection.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import OidcService from '$lib/services/oidc-service';
|
||||
@@ -17,7 +18,7 @@
|
||||
import type { ScimServiceProviderCreate } from '$lib/types/scim.type';
|
||||
import { cachedOidcClientLogo } from '$lib/utils/cached-image-util';
|
||||
import { axiosErrorToast } from '$lib/utils/error-util';
|
||||
import { LucideChevronLeft, LucideInfo, LucideRefreshCcw } from '@lucide/svelte';
|
||||
import { LucideChevronLeft, LucideInfo, LucideRefreshCcw, LucideTriangleAlert } from '@lucide/svelte';
|
||||
import { toast } from 'svelte-sonner';
|
||||
import { slide } from 'svelte/transition';
|
||||
import { backNavigate } from '../../users/navigate-back-util';
|
||||
@@ -230,122 +231,176 @@
|
||||
>
|
||||
</div>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>{client.name}</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<div class="flex flex-col">
|
||||
<div class="mb-2 flex flex-col sm:flex-row sm:items-center">
|
||||
<Field.Label class="w-52">{m.client_id()}</Field.Label>
|
||||
<CopyToClipboard value={client.id}>
|
||||
<span class="text-muted-foreground text-sm" data-testid="client-id"> {client.id}</span>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
{#if !client.isPublic}
|
||||
<div class="mt-1 mb-2 flex flex-col sm:flex-row sm:items-center">
|
||||
<Field.Label class="w-52">{m.client_secret()}</Field.Label>
|
||||
{#if $clientSecretStore}
|
||||
<CopyToClipboard value={$clientSecretStore}>
|
||||
<span class="text-muted-foreground text-sm" data-testid="client-secret">
|
||||
{$clientSecretStore}
|
||||
<Tabs.Root value="general" useHash class="gap-4">
|
||||
<div class="overflow-x-auto pb-1">
|
||||
<Tabs.List variant="line" class="min-w-max">
|
||||
<Tabs.Trigger value="general">{m.general()}</Tabs.Trigger>
|
||||
<Tabs.Trigger value="user-groups">
|
||||
{m.allowed_user_groups()}
|
||||
{#if client.isGroupRestricted && client.allowedUserGroupIds.length === 0}
|
||||
<LucideTriangleAlert class="ml-0.5 size-4 text-yellow-600 dark:text-yellow-400" />
|
||||
{/if}</Tabs.Trigger
|
||||
>
|
||||
<Tabs.Trigger value="api-access">{m.api_access()}</Tabs.Trigger>
|
||||
<Tabs.Trigger value="scim">{m.scim_provisioning()}</Tabs.Trigger>
|
||||
<Tabs.Trigger value="preview">{m.oidc_data_preview()}</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
</div>
|
||||
|
||||
<Tabs.Content value="general" class="flex flex-col gap-4">
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>{client.name}</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<div class="flex flex-col">
|
||||
<div class="mb-2 flex flex-col sm:flex-row sm:items-center">
|
||||
<Field.Label class="w-52">{m.client_id()}</Field.Label>
|
||||
<CopyToClipboard value={client.id}>
|
||||
<span class="text-muted-foreground text-sm" data-testid="client-id">
|
||||
{client.id}
|
||||
</span>
|
||||
</CopyToClipboard>
|
||||
{:else}
|
||||
<div>
|
||||
<span class="text-muted-foreground text-sm" data-testid="client-secret"
|
||||
>••••••••••••••••••••••••••••••••</span
|
||||
>
|
||||
<Button
|
||||
class="ml-2"
|
||||
onclick={createClientSecret}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
aria-label="Create new client secret"><LucideRefreshCcw class="size-3" /></Button
|
||||
</div>
|
||||
{#if !client.isPublic}
|
||||
<div class="mt-1 mb-2 flex flex-col sm:flex-row sm:items-center">
|
||||
<Field.Label class="w-52">{m.client_secret()}</Field.Label>
|
||||
{#if $clientSecretStore}
|
||||
<CopyToClipboard value={$clientSecretStore}>
|
||||
<span class="text-muted-foreground text-sm" data-testid="client-secret">
|
||||
{$clientSecretStore}
|
||||
</span>
|
||||
</CopyToClipboard>
|
||||
{:else}
|
||||
<div>
|
||||
<span class="text-muted-foreground text-sm" data-testid="client-secret"
|
||||
>••••••••••••••••••••••••••••••••</span
|
||||
>
|
||||
<Button
|
||||
class="ml-2"
|
||||
onclick={createClientSecret}
|
||||
size="sm"
|
||||
variant="ghost"
|
||||
aria-label="Create new client secret"
|
||||
><LucideRefreshCcw class="size-3" /></Button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if showAllDetails}
|
||||
<div transition:slide>
|
||||
{#each Object.entries(setupDetails) as [key, value]}
|
||||
<div class="mb-2 flex flex-col sm:flex-row sm:items-center">
|
||||
<Field.Label class="w-52">{key}</Field.Label>
|
||||
<CopyToClipboard {value}>
|
||||
<span class="text-muted-foreground text-sm">{value}</span>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if !showAllDetails}
|
||||
<div class="mt-4 flex justify-center">
|
||||
<Button onclick={() => (showAllDetails = true)} size="sm" variant="ghost"
|
||||
>{m.show_more_details()}</Button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
{/if}
|
||||
{#if showAllDetails}
|
||||
<div transition:slide>
|
||||
{#each Object.entries(setupDetails) as [key, value]}
|
||||
<div class="mb-2 flex flex-col sm:flex-row sm:items-center">
|
||||
<Field.Label class="w-52">{key}</Field.Label>
|
||||
<CopyToClipboard {value}>
|
||||
<span class="text-muted-foreground text-sm">{value}</span>
|
||||
</CopyToClipboard>
|
||||
</div>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
{#if !showAllDetails}
|
||||
<div class="mt-4 flex justify-center">
|
||||
<Button onclick={() => (showAllDetails = true)} size="sm" variant="ghost"
|
||||
>{m.show_more_details()}</Button
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
<Card.Root>
|
||||
<Card.Content>
|
||||
<OidcForm mode="update" existingClient={client} callback={updateClient} />
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
<CollapsibleCard
|
||||
id="allowed-user-groups"
|
||||
title={m.allowed_user_groups()}
|
||||
button={!client.isGroupRestricted ? UnrestrictButton : undefined}
|
||||
forcedExpanded={client.isGroupRestricted ? undefined : false}
|
||||
description={client.isGroupRestricted
|
||||
? m.allowed_user_groups_description()
|
||||
: m.allowed_user_groups_status_unrestricted_description()}
|
||||
>
|
||||
<UserGroupSelection
|
||||
bind:selectedGroupIds={client.allowedUserGroupIds}
|
||||
selectionDisabled={!client.isGroupRestricted}
|
||||
/>
|
||||
<div class="mt-5 flex justify-end gap-3">
|
||||
<Button onclick={disableGroupRestriction} variant="secondary">{m.unrestrict()}</Button>
|
||||
<Card.Root>
|
||||
<Card.Content>
|
||||
<OidcForm mode="update" existingClient={client} callback={updateClient} />
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</Tabs.Content>
|
||||
|
||||
<Button usePromiseLoading onclick={() => updateUserGroupClients(client.allowedUserGroupIds)}
|
||||
>{m.save()}</Button
|
||||
>
|
||||
</div>
|
||||
</CollapsibleCard>
|
||||
<CollapsibleCard id="api-access" title={m.api_access()} description={m.api_access_description()}>
|
||||
<ApiAccessCard clientId={client.id} isPublicClient={client.isPublic} />
|
||||
</CollapsibleCard>
|
||||
<CollapsibleCard
|
||||
id="scim-provisioning"
|
||||
title={m.scim_provisioning()}
|
||||
description={m.scim_provisioning_description()}
|
||||
>
|
||||
<ScimResourceProviderForm
|
||||
oidcClientId={client.id}
|
||||
existingProvider={scimServiceProvider}
|
||||
onSave={saveScimServiceProvider}
|
||||
/>
|
||||
</CollapsibleCard>
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<div class="flex flex-col items-start justify-between gap-3 sm:flex-row sm:items-center">
|
||||
<div>
|
||||
<Card.Title>
|
||||
{m.oidc_data_preview()}
|
||||
</Card.Title>
|
||||
<Tabs.Content value="user-groups" id="allowed-user-groups">
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<div class="flex items-center justify-between gap-4">
|
||||
<div>
|
||||
<Card.Title>{m.allowed_user_groups()}</Card.Title>
|
||||
<Card.Description>
|
||||
{client.isGroupRestricted
|
||||
? m.allowed_user_groups_description()
|
||||
: m.allowed_user_groups_status_unrestricted_description()}
|
||||
</Card.Description>
|
||||
</div>
|
||||
{#if !client.isGroupRestricted}
|
||||
{@render UnrestrictButton()}
|
||||
{/if}
|
||||
</div>
|
||||
</Card.Header>
|
||||
{#if client.isGroupRestricted}
|
||||
<Card.Content>
|
||||
<UserGroupSelection bind:selectedGroupIds={client.allowedUserGroupIds} />
|
||||
<div class="mt-5 flex justify-end gap-3">
|
||||
<Button onclick={disableGroupRestriction} variant="secondary">{m.unrestrict()}</Button>
|
||||
|
||||
<Button
|
||||
usePromiseLoading
|
||||
onclick={() => updateUserGroupClients(client.allowedUserGroupIds)}>{m.save()}</Button
|
||||
>
|
||||
</div>
|
||||
</Card.Content>
|
||||
{/if}
|
||||
</Card.Root>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="api-access" id="api-access">
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>{m.api_access()}</Card.Title>
|
||||
<Card.Description>{m.api_access_description()}</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<ApiAccessCard clientId={client.id} isPublicClient={client.isPublic} />
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="scim" id="scim-provisioning">
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>{m.scim_provisioning()}</Card.Title>
|
||||
<Card.Description>
|
||||
{m.preview_the_oidc_data_that_would_be_sent_for_different_users()}
|
||||
<FormattedMessage m={m.scim_provisioning_description()} />
|
||||
</Card.Description>
|
||||
</div>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<ScimResourceProviderForm
|
||||
oidcClientId={client.id}
|
||||
existingProvider={scimServiceProvider}
|
||||
onSave={saveScimServiceProvider}
|
||||
/>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</Tabs.Content>
|
||||
|
||||
<Button variant="outline" onclick={() => (showPreview = true)}>
|
||||
{m.show()}
|
||||
</Button>
|
||||
</div>
|
||||
</Card.Header>
|
||||
</Card.Root>
|
||||
<Tabs.Content value="preview">
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<div class="flex flex-col items-start justify-between gap-3 sm:flex-row sm:items-center">
|
||||
<div>
|
||||
<Card.Title>
|
||||
{m.oidc_data_preview()}
|
||||
</Card.Title>
|
||||
<Card.Description>
|
||||
{m.preview_the_oidc_data_that_would_be_sent_for_different_users()}
|
||||
</Card.Description>
|
||||
</div>
|
||||
|
||||
<Button variant="outline" onclick={() => (showPreview = true)}>
|
||||
{m.show()}
|
||||
</Button>
|
||||
</div>
|
||||
</Card.Header>
|
||||
</Card.Root>
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
<OidcClientPreviewModal bind:open={showPreview} clientId={client.id} />
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
<script lang="ts">
|
||||
import CollapsibleCard from '$lib/components/collapsible-card.svelte';
|
||||
import CustomClaimsInput from '$lib/components/form/custom-claims-input.svelte';
|
||||
import { Badge } from '$lib/components/ui/badge';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import * as Tabs from '$lib/components/ui/tabs';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import CustomClaimService from '$lib/services/custom-claim-service';
|
||||
import UserGroupService from '$lib/services/user-group-service';
|
||||
@@ -86,59 +86,84 @@
|
||||
<Badge class="rounded-full" variant="default">{m.ldap()}</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>{m.general()}</Card.Title>
|
||||
</Card.Header>
|
||||
|
||||
<Card.Content>
|
||||
<UserGroupForm existingUserGroup={userGroup} callback={updateUserGroup} />
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>{m.users()}</Card.Title>
|
||||
<Card.Description>{m.assign_users_to_this_group()}</Card.Description>
|
||||
</Card.Header>
|
||||
|
||||
<Card.Content>
|
||||
<UserSelection
|
||||
bind:selectedUserIds={userGroup.userIds}
|
||||
selectionDisabled={!!userGroup.ldapId && $appConfigStore.ldapEnabled}
|
||||
/>
|
||||
<div class="mt-5 flex justify-end">
|
||||
<Button
|
||||
disabled={!!userGroup.ldapId && $appConfigStore.ldapEnabled}
|
||||
onclick={() => updateUserGroupUsers(userGroup.userIds)}>{m.save()}</Button
|
||||
>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<CollapsibleCard
|
||||
id="user-group-custom-claims"
|
||||
title={m.custom_claims()}
|
||||
description={m.custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user_prioritized()}
|
||||
>
|
||||
<CustomClaimsInput bind:customClaims={userGroup.customClaims} />
|
||||
<div class="mt-5 flex justify-end">
|
||||
<Button onclick={updateCustomClaims} type="submit">{m.save()}</Button>
|
||||
<Tabs.Root value="general" useHash class="gap-4">
|
||||
<div class="overflow-x-auto pb-1">
|
||||
<Tabs.List variant="line" class="min-w-max">
|
||||
<Tabs.Trigger value="general">{m.general()}</Tabs.Trigger>
|
||||
<Tabs.Trigger value="users">{m.users()}</Tabs.Trigger>
|
||||
<Tabs.Trigger value="clients">{m.allowed_oidc_clients()}</Tabs.Trigger>
|
||||
<Tabs.Trigger value="custom-claims">{m.custom_claims()}</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
</div>
|
||||
</CollapsibleCard>
|
||||
|
||||
<CollapsibleCard
|
||||
id="user-group-oidc-clients"
|
||||
title={m.allowed_oidc_clients()}
|
||||
description={m.allowed_oidc_clients_description()}
|
||||
>
|
||||
<OidcClientSelection
|
||||
bind:this={oidcClientSelectionRef}
|
||||
bind:selectedGroupIds={userGroup.allowedOidcClientIds}
|
||||
/>
|
||||
<div class="mt-5 flex justify-end gap-3">
|
||||
<Button onclick={() => updateAllowedOidcClients(userGroup.allowedOidcClientIds)}
|
||||
>{m.save()}</Button
|
||||
>
|
||||
</div>
|
||||
</CollapsibleCard>
|
||||
<Tabs.Content value="general">
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>{m.general()}</Card.Title>
|
||||
</Card.Header>
|
||||
|
||||
<Card.Content>
|
||||
<UserGroupForm existingUserGroup={userGroup} callback={updateUserGroup} />
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="users">
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>{m.users()}</Card.Title>
|
||||
<Card.Description>{m.assign_users_to_this_group()}</Card.Description>
|
||||
</Card.Header>
|
||||
|
||||
<Card.Content>
|
||||
<UserSelection
|
||||
bind:selectedUserIds={userGroup.userIds}
|
||||
selectionDisabled={!!userGroup.ldapId && $appConfigStore.ldapEnabled}
|
||||
/>
|
||||
<div class="mt-5 flex justify-end">
|
||||
<Button
|
||||
disabled={!!userGroup.ldapId && $appConfigStore.ldapEnabled}
|
||||
onclick={() => updateUserGroupUsers(userGroup.userIds)}>{m.save()}</Button
|
||||
>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="clients" id="user-group-oidc-clients">
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>{m.allowed_oidc_clients()}</Card.Title>
|
||||
<Card.Description>{m.allowed_oidc_clients_description()}</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<OidcClientSelection
|
||||
bind:this={oidcClientSelectionRef}
|
||||
bind:selectedGroupIds={userGroup.allowedOidcClientIds}
|
||||
/>
|
||||
<div class="mt-5 flex justify-end gap-3">
|
||||
<Button onclick={() => updateAllowedOidcClients(userGroup.allowedOidcClientIds)}
|
||||
>{m.save()}</Button
|
||||
>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="custom-claims" id="user-group-custom-claims">
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>{m.custom_claims()}</Card.Title>
|
||||
<Card.Description>
|
||||
{m.custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user_prioritized()}
|
||||
</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<CustomClaimsInput bind:customClaims={userGroup.customClaims} />
|
||||
<div class="mt-5 flex justify-end">
|
||||
<Button onclick={updateCustomClaims} type="submit">{m.save()}</Button>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
<script lang="ts">
|
||||
import CollapsibleCard from '$lib/components/collapsible-card.svelte';
|
||||
import CustomClaimsInput from '$lib/components/form/custom-claims-input.svelte';
|
||||
import ProfilePictureSettings from '$lib/components/form/profile-picture-settings.svelte';
|
||||
import Badge from '$lib/components/ui/badge/badge.svelte';
|
||||
import { Button } from '$lib/components/ui/button';
|
||||
import * as Card from '$lib/components/ui/card';
|
||||
import * as Item from '$lib/components/ui/item/index.js';
|
||||
import * as Tabs from '$lib/components/ui/tabs';
|
||||
import UserGroupSelection from '$lib/components/user-group-selection.svelte';
|
||||
import { m } from '$lib/paraglide/messages';
|
||||
import CustomClaimService from '$lib/services/custom-claim-service';
|
||||
@@ -94,70 +94,95 @@
|
||||
<Badge class="rounded-full" variant="default">{m.ldap()}</Badge>
|
||||
{/if}
|
||||
</div>
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>{m.general()}</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<UserForm existingUser={user} callback={updateUser} />
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<Card.Root>
|
||||
<Card.Content>
|
||||
<ProfilePictureSettings
|
||||
userId={user.id}
|
||||
isLdapUser={!!user.ldapId}
|
||||
updateCallback={updateProfilePicture}
|
||||
resetCallback={resetProfilePicture}
|
||||
/>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<CollapsibleCard
|
||||
id="user-groups"
|
||||
title={m.user_groups()}
|
||||
description={m.manage_which_groups_this_user_belongs_to()}
|
||||
>
|
||||
<UserGroupSelection
|
||||
bind:selectedGroupIds={user.userGroupIds}
|
||||
selectionDisabled={!!user.ldapId && $appConfigStore.ldapEnabled}
|
||||
/>
|
||||
<div class="mt-5 flex justify-end">
|
||||
<Button
|
||||
onclick={() => updateUserGroups(user.userGroupIds)}
|
||||
disabled={!!user.ldapId && $appConfigStore.ldapEnabled}
|
||||
type="submit">{m.save()}</Button
|
||||
>
|
||||
<Tabs.Root value="general" useHash class="gap-4">
|
||||
<div class="overflow-x-auto pb-1">
|
||||
<Tabs.List variant="line" class="min-w-max">
|
||||
<Tabs.Trigger value="general">{m.general()}</Tabs.Trigger>
|
||||
<Tabs.Trigger value="groups">{m.user_groups()}</Tabs.Trigger>
|
||||
<Tabs.Trigger value="passkeys">{m.passkeys()}</Tabs.Trigger>
|
||||
<Tabs.Trigger value="custom-claims">{m.custom_claims()}</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
</div>
|
||||
</CollapsibleCard>
|
||||
|
||||
<Item.Group class="bg-card border shadow-sm rounded-4xl p-5">
|
||||
<Item.Root class="border-none bg-transparent p-0">
|
||||
<Item.Media class="text-primary/80">
|
||||
<KeyRound class="size-5" />
|
||||
</Item.Media>
|
||||
<Item.Content class="min-w-52">
|
||||
<Item.Title class="text-xl font-semibold">{m.passkeys()}</Item.Title>
|
||||
<Item.Description
|
||||
>{passkeys.length > 0
|
||||
? m.manage_this_users_passkeys()
|
||||
: m.user_has_no_passkeys_yet()}</Item.Description
|
||||
>
|
||||
</Item.Content>
|
||||
</Item.Root>
|
||||
{#if passkeys.length > 0}
|
||||
<AdminPasskeyList userId={user.id} bind:passkeys />
|
||||
{/if}
|
||||
</Item.Group>
|
||||
<Tabs.Content value="general" class="flex flex-col gap-4">
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>{m.general()}</Card.Title>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<UserForm existingUser={user} callback={updateUser} />
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
|
||||
<CollapsibleCard
|
||||
id="user-custom-claims"
|
||||
title={m.custom_claims()}
|
||||
description={m.custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user()}
|
||||
>
|
||||
<CustomClaimsInput bind:customClaims={user.customClaims} />
|
||||
<div class="mt-5 flex justify-end">
|
||||
<Button onclick={updateCustomClaims} type="submit">{m.save()}</Button>
|
||||
</div>
|
||||
</CollapsibleCard>
|
||||
<Card.Root>
|
||||
<Card.Content>
|
||||
<ProfilePictureSettings
|
||||
userId={user.id}
|
||||
isLdapUser={!!user.ldapId}
|
||||
updateCallback={updateProfilePicture}
|
||||
resetCallback={resetProfilePicture}
|
||||
/>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="groups" id="user-groups">
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>{m.user_groups()}</Card.Title>
|
||||
<Card.Description>{m.manage_which_groups_this_user_belongs_to()}</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<UserGroupSelection
|
||||
bind:selectedGroupIds={user.userGroupIds}
|
||||
selectionDisabled={!!user.ldapId && $appConfigStore.ldapEnabled}
|
||||
/>
|
||||
<div class="mt-5 flex justify-end">
|
||||
<Button
|
||||
onclick={() => updateUserGroups(user.userGroupIds)}
|
||||
disabled={!!user.ldapId && $appConfigStore.ldapEnabled}
|
||||
type="submit">{m.save()}</Button
|
||||
>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="passkeys">
|
||||
<Item.Group class="bg-card rounded-4xl border p-5 shadow-sm">
|
||||
<Item.Root class="border-none bg-transparent p-0">
|
||||
<Item.Media class="text-primary/80">
|
||||
<KeyRound class="size-5" />
|
||||
</Item.Media>
|
||||
<Item.Content class="min-w-52">
|
||||
<Item.Title class="text-xl font-semibold">{m.passkeys()}</Item.Title>
|
||||
<Item.Description
|
||||
>{passkeys.length > 0
|
||||
? m.manage_this_users_passkeys()
|
||||
: m.user_has_no_passkeys_yet()}</Item.Description
|
||||
>
|
||||
</Item.Content>
|
||||
</Item.Root>
|
||||
{#if passkeys.length > 0}
|
||||
<AdminPasskeyList userId={user.id} bind:passkeys />
|
||||
{/if}
|
||||
</Item.Group>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="custom-claims" id="user-custom-claims">
|
||||
<Card.Root>
|
||||
<Card.Header>
|
||||
<Card.Title>{m.custom_claims()}</Card.Title>
|
||||
<Card.Description>
|
||||
{m.custom_claims_are_key_value_pairs_that_can_be_used_to_store_additional_information_about_a_user()}
|
||||
</Card.Description>
|
||||
</Card.Header>
|
||||
<Card.Content>
|
||||
<CustomClaimsInput bind:customClaims={user.customClaims} />
|
||||
<div class="mt-5 flex justify-end">
|
||||
<Button onclick={updateCustomClaims} type="submit">{m.save()}</Button>
|
||||
</div>
|
||||
</Card.Content>
|
||||
</Card.Root>
|
||||
</Tabs.Content>
|
||||
</Tabs.Root>
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
</script>
|
||||
|
||||
<Card.Root
|
||||
class="border-muted group relative h-[160px] p-5 transition-all duration-200 hover:shadow-md sm:max-w-[50vw] md:max-w-[400px]"
|
||||
class="border-muted group relative h-[160px] p-5 transition-all duration-200 hover:shadow-md sm:max-w-[50vw] md:max-w-[430px]"
|
||||
data-testid="authorized-oidc-client-card"
|
||||
>
|
||||
<Card.Content class=" p-0">
|
||||
|
||||
Reference in New Issue
Block a user