revoke invites and remove users from organizations

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-06-14 21:56:29 +02:00
parent 1dcd501b13
commit ee88c9e391
28 changed files with 220 additions and 90 deletions

View File

@@ -4,7 +4,7 @@ import { memo } from 'react';
import { ChartSwitch } from '@/components/report/chart';
import { Widget, WidgetBody, WidgetHead } from '@/components/widget';
import type { IChartInput } from '@openpanel/validation';
import type { IChartProps } from '@openpanel/validation';
type Props = {
profileId: string;
@@ -12,7 +12,7 @@ type Props = {
};
const ProfileCharts = ({ profileId, projectId }: Props) => {
const pageViewsChart: IChartInput = {
const pageViewsChart: IChartProps = {
projectId,
chartType: 'linear',
events: [
@@ -45,7 +45,7 @@ const ProfileCharts = ({ profileId, projectId }: Props) => {
metric: 'sum',
};
const eventsChart: IChartInput = {
const eventsChart: IChartProps = {
projectId,
chartType: 'linear',
events: [

View File

@@ -9,7 +9,7 @@ import { api } from '@/trpc/client';
import { cn } from '@/utils/cn';
import dynamic from 'next/dynamic';
import type { IChartInput } from '@openpanel/validation';
import type { IChartProps } from '@openpanel/validation';
interface RealtimeLiveHistogramProps {
projectId: string;
@@ -18,20 +18,18 @@ interface RealtimeLiveHistogramProps {
export function RealtimeLiveHistogram({
projectId,
}: RealtimeLiveHistogramProps) {
const report: IChartInput = {
const report: IChartProps = {
projectId,
events: [
{
segment: 'user',
filters: [
{
id: '1',
name: 'name',
operator: 'is',
value: ['screen_view', 'session_start'],
},
],
id: 'A',
name: '*',
displayName: 'Active users',
},
@@ -45,7 +43,7 @@ export function RealtimeLiveHistogram({
lineType: 'monotone',
previous: false,
};
const countReport: IChartInput = {
const countReport: IChartProps = {
name: '',
projectId,
events: [
@@ -85,7 +83,7 @@ export function RealtimeLiveHistogram({
{staticArray.map((percent, i) => (
<div
key={i}
className="bg-def-200 flex-1 animate-pulse rounded-md"
className="flex-1 animate-pulse rounded-md bg-def-200"
style={{ height: `${percent}%` }}
/>
))}

View File

@@ -42,6 +42,7 @@ export default function EditOrganization({
return (
<form
className="opacity-50"
onSubmit={handleSubmit((values) => {
mutation.mutate(values);
})}
@@ -49,12 +50,18 @@ export default function EditOrganization({
<Widget>
<WidgetHead className="flex items-center justify-between">
<span className="title">Org. details</span>
<Button size="sm" type="submit" disabled={!formState.isDirty}>
<Button
size="sm"
type="submit"
disabled
// disabled={!formState.isDirty}
>
Save
</Button>
</WidgetHead>
<WidgetBody>
<InputWithLabel
disabled
label="Name"
{...register('name')}
defaultValue={organization?.name}

View File

@@ -3,6 +3,13 @@
import { Dot } from '@/components/dot';
import { TooltipComplete } from '@/components/tooltip-complete';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
Table,
TableBody,
@@ -12,7 +19,11 @@ import {
TableRow,
} from '@/components/ui/table';
import { Widget, WidgetHead } from '@/components/widget';
import { api } from '@/trpc/client';
import { cn } from '@/utils/cn';
import { MoreHorizontalIcon } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { toast } from 'sonner';
import type { IServiceInvite, IServiceProject } from '@openpanel/db';
@@ -38,6 +49,7 @@ const Invites = ({ invites, projects }: Props) => {
<TableHead>Created</TableHead>
<TableHead>Status</TableHead>
<TableHead>Access</TableHead>
<TableHead>More</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -62,8 +74,19 @@ function Item({
projects,
publicMetadata,
status,
organizationId,
}: ItemProps) {
const router = useRouter();
const access = (publicMetadata?.access ?? []) as string[];
const revoke = api.organization.revokeInvite.useMutation({
onSuccess() {
toast.success(`Invite for ${email} revoked`);
router.refresh();
},
onError() {
toast.error(`Failed to revoke invite for ${email}`);
},
});
return (
<TableRow key={id}>
<TableCell className="font-medium">{email}</TableCell>
@@ -104,6 +127,23 @@ function Item({
<Badge variant={'secondary'}>All projects</Badge>
)}
</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button icon={MoreHorizontalIcon} size="icon" variant={'outline'} />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
className="text-destructive"
onClick={() => {
revoke.mutate({ organizationId, invitationId: id });
}}
>
Revoke invite
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
);
}

View File

@@ -1,7 +1,14 @@
'use client';
import { useState } from 'react';
import { Button } from '@/components/ui/button';
import { ComboboxAdvanced } from '@/components/ui/combobox-advanced';
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuTrigger,
} from '@/components/ui/dropdown-menu';
import {
Table,
TableBody,
@@ -12,6 +19,9 @@ import {
} from '@/components/ui/table';
import { Widget, WidgetHead } from '@/components/widget';
import { api } from '@/trpc/client';
import { MoreHorizontalIcon } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { toast } from 'sonner';
import type { IServiceMember, IServiceProject } from '@openpanel/db';
@@ -33,6 +43,7 @@ const Members = ({ members, projects }: Props) => {
<TableHead>Role</TableHead>
<TableHead>Created</TableHead>
<TableHead>Access</TableHead>
<TableHead>More</TableHead>
</TableRow>
</TableHeader>
<TableBody>
@@ -58,7 +69,17 @@ function Item({
projects,
access: prevAccess,
}: ItemProps) {
const router = useRouter();
const mutation = api.organization.updateMemberAccess.useMutation();
const revoke = api.organization.removeMember.useMutation({
onSuccess() {
toast.success(`${name} has been removed from the organization`);
router.refresh();
},
onError() {
toast.error(`Failed to remove ${name} from the organization`);
},
});
const [access, setAccess] = useState<string[]>(
prevAccess.map((item) => item.projectId)
);
@@ -86,6 +107,23 @@ function Item({
}))}
/>
</TableCell>
<TableCell>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button icon={MoreHorizontalIcon} size="icon" variant={'outline'} />
</DropdownMenuTrigger>
<DropdownMenuContent>
<DropdownMenuItem
className="text-destructive"
onClick={() => {
revoke.mutate({ organizationId: organization.id, userId: id! });
}}
>
Remove member
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</TableCell>
</TableRow>
);
}

View File

@@ -1,4 +1,7 @@
import PageLayout from '@/app/(app)/[organizationSlug]/[projectId]/page-layout';
import { FullPageEmptyState } from '@/components/full-page-empty-state';
import { auth, clerkClient } from '@clerk/nextjs/server';
import { ShieldAlertIcon } from 'lucide-react';
import { notFound } from 'next/navigation';
import { getOrganizationBySlug } from '@openpanel/db';
@@ -17,26 +20,49 @@ export default async function Page({
params: { organizationSlug },
}: PageProps) {
const organization = await getOrganizationBySlug(organizationSlug);
const session = auth();
const memberships = await clerkClient.users.getOrganizationMembershipList({
userId: session.userId!,
});
if (!organization) {
return notFound();
}
const member = memberships.data.find(
(membership) => membership.organization.id === organization.id
);
if (!member) {
return notFound();
}
const hasAccess = member.role === 'org:admin';
return (
<>
<PageLayout
title={organization.name}
organizationSlug={organizationSlug}
/>
<div className="grid gap-8 p-4 lg:grid-cols-2">
<EditOrganization organization={organization} />
<div className="col-span-2">
<MembersServer organizationSlug={organizationSlug} />
{hasAccess ? (
<div className="grid gap-8 p-4 lg:grid-cols-2">
<EditOrganization organization={organization} />
<div className="col-span-2">
<MembersServer organizationSlug={organizationSlug} />
</div>
<div className="col-span-2">
<InvitesServer organizationSlug={organizationSlug} />
</div>
</div>
<div className="col-span-2">
<InvitesServer organizationSlug={organizationSlug} />
</div>
</div>
) : (
<>
<FullPageEmptyState icon={ShieldAlertIcon} title="No access">
You do not have access to this page. You need to be an admin of this
organization to access this page.
</FullPageEmptyState>
</>
)}
</>
);
}