revoke invites and remove users from organizations
This commit is contained in:
@@ -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: [
|
||||
|
||||
@@ -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}%` }}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user