revoke invites and remove users from organizations
This commit is contained in:
@@ -151,9 +151,7 @@ export async function charts(
|
|||||||
|
|
||||||
return getChart({
|
return getChart({
|
||||||
...query.data,
|
...query.data,
|
||||||
name: 'export-api',
|
|
||||||
metric: 'sum',
|
|
||||||
lineType: 'monotone',
|
|
||||||
chartType: 'linear',
|
chartType: 'linear',
|
||||||
|
metric: 'sum',
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,7 @@ import { memo } from 'react';
|
|||||||
import { ChartSwitch } from '@/components/report/chart';
|
import { ChartSwitch } from '@/components/report/chart';
|
||||||
import { Widget, WidgetBody, WidgetHead } from '@/components/widget';
|
import { Widget, WidgetBody, WidgetHead } from '@/components/widget';
|
||||||
|
|
||||||
import type { IChartInput } from '@openpanel/validation';
|
import type { IChartProps } from '@openpanel/validation';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
profileId: string;
|
profileId: string;
|
||||||
@@ -12,7 +12,7 @@ type Props = {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const ProfileCharts = ({ profileId, projectId }: Props) => {
|
const ProfileCharts = ({ profileId, projectId }: Props) => {
|
||||||
const pageViewsChart: IChartInput = {
|
const pageViewsChart: IChartProps = {
|
||||||
projectId,
|
projectId,
|
||||||
chartType: 'linear',
|
chartType: 'linear',
|
||||||
events: [
|
events: [
|
||||||
@@ -45,7 +45,7 @@ const ProfileCharts = ({ profileId, projectId }: Props) => {
|
|||||||
metric: 'sum',
|
metric: 'sum',
|
||||||
};
|
};
|
||||||
|
|
||||||
const eventsChart: IChartInput = {
|
const eventsChart: IChartProps = {
|
||||||
projectId,
|
projectId,
|
||||||
chartType: 'linear',
|
chartType: 'linear',
|
||||||
events: [
|
events: [
|
||||||
|
|||||||
@@ -9,7 +9,7 @@ import { api } from '@/trpc/client';
|
|||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import dynamic from 'next/dynamic';
|
import dynamic from 'next/dynamic';
|
||||||
|
|
||||||
import type { IChartInput } from '@openpanel/validation';
|
import type { IChartProps } from '@openpanel/validation';
|
||||||
|
|
||||||
interface RealtimeLiveHistogramProps {
|
interface RealtimeLiveHistogramProps {
|
||||||
projectId: string;
|
projectId: string;
|
||||||
@@ -18,20 +18,18 @@ interface RealtimeLiveHistogramProps {
|
|||||||
export function RealtimeLiveHistogram({
|
export function RealtimeLiveHistogram({
|
||||||
projectId,
|
projectId,
|
||||||
}: RealtimeLiveHistogramProps) {
|
}: RealtimeLiveHistogramProps) {
|
||||||
const report: IChartInput = {
|
const report: IChartProps = {
|
||||||
projectId,
|
projectId,
|
||||||
events: [
|
events: [
|
||||||
{
|
{
|
||||||
segment: 'user',
|
segment: 'user',
|
||||||
filters: [
|
filters: [
|
||||||
{
|
{
|
||||||
id: '1',
|
|
||||||
name: 'name',
|
name: 'name',
|
||||||
operator: 'is',
|
operator: 'is',
|
||||||
value: ['screen_view', 'session_start'],
|
value: ['screen_view', 'session_start'],
|
||||||
},
|
},
|
||||||
],
|
],
|
||||||
id: 'A',
|
|
||||||
name: '*',
|
name: '*',
|
||||||
displayName: 'Active users',
|
displayName: 'Active users',
|
||||||
},
|
},
|
||||||
@@ -45,7 +43,7 @@ export function RealtimeLiveHistogram({
|
|||||||
lineType: 'monotone',
|
lineType: 'monotone',
|
||||||
previous: false,
|
previous: false,
|
||||||
};
|
};
|
||||||
const countReport: IChartInput = {
|
const countReport: IChartProps = {
|
||||||
name: '',
|
name: '',
|
||||||
projectId,
|
projectId,
|
||||||
events: [
|
events: [
|
||||||
@@ -85,7 +83,7 @@ export function RealtimeLiveHistogram({
|
|||||||
{staticArray.map((percent, i) => (
|
{staticArray.map((percent, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
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}%` }}
|
style={{ height: `${percent}%` }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export default function EditOrganization({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
|
className="opacity-50"
|
||||||
onSubmit={handleSubmit((values) => {
|
onSubmit={handleSubmit((values) => {
|
||||||
mutation.mutate(values);
|
mutation.mutate(values);
|
||||||
})}
|
})}
|
||||||
@@ -49,12 +50,18 @@ export default function EditOrganization({
|
|||||||
<Widget>
|
<Widget>
|
||||||
<WidgetHead className="flex items-center justify-between">
|
<WidgetHead className="flex items-center justify-between">
|
||||||
<span className="title">Org. details</span>
|
<span className="title">Org. details</span>
|
||||||
<Button size="sm" type="submit" disabled={!formState.isDirty}>
|
<Button
|
||||||
|
size="sm"
|
||||||
|
type="submit"
|
||||||
|
disabled
|
||||||
|
// disabled={!formState.isDirty}
|
||||||
|
>
|
||||||
Save
|
Save
|
||||||
</Button>
|
</Button>
|
||||||
</WidgetHead>
|
</WidgetHead>
|
||||||
<WidgetBody>
|
<WidgetBody>
|
||||||
<InputWithLabel
|
<InputWithLabel
|
||||||
|
disabled
|
||||||
label="Name"
|
label="Name"
|
||||||
{...register('name')}
|
{...register('name')}
|
||||||
defaultValue={organization?.name}
|
defaultValue={organization?.name}
|
||||||
|
|||||||
@@ -3,6 +3,13 @@
|
|||||||
import { Dot } from '@/components/dot';
|
import { Dot } from '@/components/dot';
|
||||||
import { TooltipComplete } from '@/components/tooltip-complete';
|
import { TooltipComplete } from '@/components/tooltip-complete';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/dropdown-menu';
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -12,7 +19,11 @@ import {
|
|||||||
TableRow,
|
TableRow,
|
||||||
} from '@/components/ui/table';
|
} from '@/components/ui/table';
|
||||||
import { Widget, WidgetHead } from '@/components/widget';
|
import { Widget, WidgetHead } from '@/components/widget';
|
||||||
|
import { api } from '@/trpc/client';
|
||||||
import { cn } from '@/utils/cn';
|
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';
|
import type { IServiceInvite, IServiceProject } from '@openpanel/db';
|
||||||
|
|
||||||
@@ -38,6 +49,7 @@ const Invites = ({ invites, projects }: Props) => {
|
|||||||
<TableHead>Created</TableHead>
|
<TableHead>Created</TableHead>
|
||||||
<TableHead>Status</TableHead>
|
<TableHead>Status</TableHead>
|
||||||
<TableHead>Access</TableHead>
|
<TableHead>Access</TableHead>
|
||||||
|
<TableHead>More</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -62,8 +74,19 @@ function Item({
|
|||||||
projects,
|
projects,
|
||||||
publicMetadata,
|
publicMetadata,
|
||||||
status,
|
status,
|
||||||
|
organizationId,
|
||||||
}: ItemProps) {
|
}: ItemProps) {
|
||||||
|
const router = useRouter();
|
||||||
const access = (publicMetadata?.access ?? []) as string[];
|
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 (
|
return (
|
||||||
<TableRow key={id}>
|
<TableRow key={id}>
|
||||||
<TableCell className="font-medium">{email}</TableCell>
|
<TableCell className="font-medium">{email}</TableCell>
|
||||||
@@ -104,6 +127,23 @@ function Item({
|
|||||||
<Badge variant={'secondary'}>All projects</Badge>
|
<Badge variant={'secondary'}>All projects</Badge>
|
||||||
)}
|
)}
|
||||||
</TableCell>
|
</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>
|
</TableRow>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,14 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useState } from 'react';
|
import { useState } from 'react';
|
||||||
|
import { Button } from '@/components/ui/button';
|
||||||
import { ComboboxAdvanced } from '@/components/ui/combobox-advanced';
|
import { ComboboxAdvanced } from '@/components/ui/combobox-advanced';
|
||||||
|
import {
|
||||||
|
DropdownMenu,
|
||||||
|
DropdownMenuContent,
|
||||||
|
DropdownMenuItem,
|
||||||
|
DropdownMenuTrigger,
|
||||||
|
} from '@/components/ui/dropdown-menu';
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
@@ -12,6 +19,9 @@ import {
|
|||||||
} from '@/components/ui/table';
|
} from '@/components/ui/table';
|
||||||
import { Widget, WidgetHead } from '@/components/widget';
|
import { Widget, WidgetHead } from '@/components/widget';
|
||||||
import { api } from '@/trpc/client';
|
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';
|
import type { IServiceMember, IServiceProject } from '@openpanel/db';
|
||||||
|
|
||||||
@@ -33,6 +43,7 @@ const Members = ({ members, projects }: Props) => {
|
|||||||
<TableHead>Role</TableHead>
|
<TableHead>Role</TableHead>
|
||||||
<TableHead>Created</TableHead>
|
<TableHead>Created</TableHead>
|
||||||
<TableHead>Access</TableHead>
|
<TableHead>Access</TableHead>
|
||||||
|
<TableHead>More</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
@@ -58,7 +69,17 @@ function Item({
|
|||||||
projects,
|
projects,
|
||||||
access: prevAccess,
|
access: prevAccess,
|
||||||
}: ItemProps) {
|
}: ItemProps) {
|
||||||
|
const router = useRouter();
|
||||||
const mutation = api.organization.updateMemberAccess.useMutation();
|
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[]>(
|
const [access, setAccess] = useState<string[]>(
|
||||||
prevAccess.map((item) => item.projectId)
|
prevAccess.map((item) => item.projectId)
|
||||||
);
|
);
|
||||||
@@ -86,6 +107,23 @@ function Item({
|
|||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
</TableCell>
|
</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>
|
</TableRow>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,7 @@
|
|||||||
import PageLayout from '@/app/(app)/[organizationSlug]/[projectId]/page-layout';
|
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 { notFound } from 'next/navigation';
|
||||||
|
|
||||||
import { getOrganizationBySlug } from '@openpanel/db';
|
import { getOrganizationBySlug } from '@openpanel/db';
|
||||||
@@ -17,17 +20,32 @@ export default async function Page({
|
|||||||
params: { organizationSlug },
|
params: { organizationSlug },
|
||||||
}: PageProps) {
|
}: PageProps) {
|
||||||
const organization = await getOrganizationBySlug(organizationSlug);
|
const organization = await getOrganizationBySlug(organizationSlug);
|
||||||
|
const session = auth();
|
||||||
|
const memberships = await clerkClient.users.getOrganizationMembershipList({
|
||||||
|
userId: session.userId!,
|
||||||
|
});
|
||||||
|
|
||||||
if (!organization) {
|
if (!organization) {
|
||||||
return notFound();
|
return notFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const member = memberships.data.find(
|
||||||
|
(membership) => membership.organization.id === organization.id
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!member) {
|
||||||
|
return notFound();
|
||||||
|
}
|
||||||
|
|
||||||
|
const hasAccess = member.role === 'org:admin';
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PageLayout
|
<PageLayout
|
||||||
title={organization.name}
|
title={organization.name}
|
||||||
organizationSlug={organizationSlug}
|
organizationSlug={organizationSlug}
|
||||||
/>
|
/>
|
||||||
|
{hasAccess ? (
|
||||||
<div className="grid gap-8 p-4 lg:grid-cols-2">
|
<div className="grid gap-8 p-4 lg:grid-cols-2">
|
||||||
<EditOrganization organization={organization} />
|
<EditOrganization organization={organization} />
|
||||||
<div className="col-span-2">
|
<div className="col-span-2">
|
||||||
@@ -37,6 +55,14 @@ export default async function Page({
|
|||||||
<InvitesServer organizationSlug={organizationSlug} />
|
<InvitesServer organizationSlug={organizationSlug} />
|
||||||
</div>
|
</div>
|
||||||
</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>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,7 +13,7 @@ export function CreateClientSuccess({ id, secret, cors }: Props) {
|
|||||||
<div className="grid gap-4">
|
<div className="grid gap-4">
|
||||||
<button className="text-left" onClick={() => clipboard(id)}>
|
<button className="text-left" onClick={() => clipboard(id)}>
|
||||||
<Label>Client ID</Label>
|
<Label>Client ID</Label>
|
||||||
<div className="flex items-center justify-between rounded bg-gray-100 p-2 px-3">
|
<div className="flex items-center justify-between rounded border-input bg-background p-2 px-3 font-mono text-sm">
|
||||||
{id}
|
{id}
|
||||||
<Copy size={16} />
|
<Copy size={16} />
|
||||||
</div>
|
</div>
|
||||||
@@ -25,7 +25,7 @@ export function CreateClientSuccess({ id, secret, cors }: Props) {
|
|||||||
onClick={() => clipboard(secret)}
|
onClick={() => clipboard(secret)}
|
||||||
>
|
>
|
||||||
<Label>Client secret</Label>
|
<Label>Client secret</Label>
|
||||||
<div className="flex items-center justify-between rounded bg-gray-100 p-2 px-3">
|
<div className="flex items-center justify-between rounded border-input bg-background p-2 px-3 font-mono text-sm">
|
||||||
{secret}
|
{secret}
|
||||||
<Copy size={16} />
|
<Copy size={16} />
|
||||||
</div>
|
</div>
|
||||||
@@ -40,7 +40,7 @@ export function CreateClientSuccess({ id, secret, cors }: Props) {
|
|||||||
{cors && (
|
{cors && (
|
||||||
<div className="text-left">
|
<div className="text-left">
|
||||||
<Label>CORS settings</Label>
|
<Label>CORS settings</Label>
|
||||||
<div className="flex items-center justify-between rounded bg-gray-100 p-2 px-3">
|
<div className="flex items-center justify-between rounded border-input bg-background p-2 px-3 font-mono text-sm">
|
||||||
{cors}
|
{cors}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
import { pushModal } from '@/modals';
|
import { pushModal } from '@/modals';
|
||||||
import { ScanEyeIcon } from 'lucide-react';
|
import { ScanEyeIcon } from 'lucide-react';
|
||||||
|
|
||||||
import type { IChartInput } from '@openpanel/validation';
|
import type { IChartProps } from '@openpanel/validation';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
chart: IChartInput;
|
chart: IChartProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
const OverviewDetailsButton = ({ chart }: Props) => {
|
const OverviewDetailsButton = ({ chart }: Props) => {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
import { api } from '@/trpc/client';
|
import { api } from '@/trpc/client';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
|
|
||||||
import type { IChartInput } from '@openpanel/validation';
|
import type { IChartProps } from '@openpanel/validation';
|
||||||
|
|
||||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
|
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
|
||||||
|
|
||||||
@@ -14,7 +14,7 @@ interface OverviewLiveHistogramProps {
|
|||||||
export function OverviewLiveHistogram({
|
export function OverviewLiveHistogram({
|
||||||
projectId,
|
projectId,
|
||||||
}: OverviewLiveHistogramProps) {
|
}: OverviewLiveHistogramProps) {
|
||||||
const report: IChartInput = {
|
const report: IChartProps = {
|
||||||
projectId,
|
projectId,
|
||||||
events: [
|
events: [
|
||||||
{
|
{
|
||||||
@@ -41,7 +41,7 @@ export function OverviewLiveHistogram({
|
|||||||
lineType: 'monotone',
|
lineType: 'monotone',
|
||||||
previous: false,
|
previous: false,
|
||||||
};
|
};
|
||||||
const countReport: IChartInput = {
|
const countReport: IChartProps = {
|
||||||
name: '',
|
name: '',
|
||||||
projectId,
|
projectId,
|
||||||
events: [
|
events: [
|
||||||
@@ -81,7 +81,7 @@ export function OverviewLiveHistogram({
|
|||||||
{staticArray.map((percent, i) => (
|
{staticArray.map((percent, i) => (
|
||||||
<div
|
<div
|
||||||
key={i}
|
key={i}
|
||||||
className="bg-def-200 flex-1 animate-pulse rounded"
|
className="flex-1 animate-pulse rounded bg-def-200"
|
||||||
style={{ height: `${percent}%` }}
|
style={{ height: `${percent}%` }}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { ChartSwitch } from '@/components/report/chart';
|
|||||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
|
|
||||||
import type { IChartInput } from '@openpanel/validation';
|
import type { IChartProps } from '@openpanel/validation';
|
||||||
|
|
||||||
import { OverviewLiveHistogram } from './overview-live-histogram';
|
import { OverviewLiveHistogram } from './overview-live-histogram';
|
||||||
|
|
||||||
@@ -186,7 +186,7 @@ export default function OverviewMetrics({ projectId }: OverviewMetricsProps) {
|
|||||||
metric: 'average',
|
metric: 'average',
|
||||||
unit: 'min',
|
unit: 'min',
|
||||||
},
|
},
|
||||||
] satisfies (IChartInput & { id: string })[];
|
] satisfies (IChartProps & { id: string })[];
|
||||||
|
|
||||||
const selectedMetric = reports[metric]!;
|
const selectedMetric = reports[metric]!;
|
||||||
|
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
import { parseAsStringEnum, useQueryState } from 'nuqs';
|
import { parseAsStringEnum, useQueryState } from 'nuqs';
|
||||||
|
|
||||||
import { mapKeys } from '@openpanel/validation';
|
import { mapKeys } from '@openpanel/validation';
|
||||||
import type { IChartInput } from '@openpanel/validation';
|
import type { IChartProps } from '@openpanel/validation';
|
||||||
|
|
||||||
export function useOverviewWidget<T extends string>(
|
export function useOverviewWidget<T extends string>(
|
||||||
key: string,
|
key: string,
|
||||||
widgets: Record<
|
widgets: Record<
|
||||||
T,
|
T,
|
||||||
{ title: string; btn: string; chart: IChartInput; hide?: boolean }
|
{ title: string; btn: string; chart: IChartProps; hide?: boolean }
|
||||||
>
|
>
|
||||||
) {
|
) {
|
||||||
const keys = Object.keys(widgets) as T[];
|
const keys = Object.keys(widgets) as T[];
|
||||||
|
|||||||
@@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
import { api } from '@/trpc/client';
|
import { api } from '@/trpc/client';
|
||||||
|
|
||||||
import type { IChartInput } from '@openpanel/validation';
|
import type { IChartProps } from '@openpanel/validation';
|
||||||
|
|
||||||
import { ChartEmpty } from './ChartEmpty';
|
import { ChartEmpty } from './ChartEmpty';
|
||||||
import { ReportAreaChart } from './ReportAreaChart';
|
import { ReportAreaChart } from './ReportAreaChart';
|
||||||
@@ -13,7 +13,7 @@ import { ReportMapChart } from './ReportMapChart';
|
|||||||
import { ReportMetricChart } from './ReportMetricChart';
|
import { ReportMetricChart } from './ReportMetricChart';
|
||||||
import { ReportPieChart } from './ReportPieChart';
|
import { ReportPieChart } from './ReportPieChart';
|
||||||
|
|
||||||
export type ReportChartProps = IChartInput;
|
export type ReportChartProps = IChartProps;
|
||||||
|
|
||||||
export function Chart({
|
export function Chart({
|
||||||
interval,
|
interval,
|
||||||
@@ -45,20 +45,16 @@ export function Chart({
|
|||||||
|
|
||||||
const [data] = api.chart.chart.useSuspenseQuery(
|
const [data] = api.chart.chart.useSuspenseQuery(
|
||||||
{
|
{
|
||||||
// dont send lineType since it does not need to be sent
|
|
||||||
lineType: 'monotone',
|
|
||||||
interval,
|
interval,
|
||||||
chartType,
|
chartType,
|
||||||
events,
|
events,
|
||||||
breakdowns,
|
breakdowns,
|
||||||
name,
|
|
||||||
range,
|
range,
|
||||||
startDate,
|
startDate,
|
||||||
endDate,
|
endDate,
|
||||||
projectId,
|
projectId,
|
||||||
previous,
|
previous,
|
||||||
formula,
|
formula,
|
||||||
unit,
|
|
||||||
metric,
|
metric,
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -11,12 +11,12 @@ import {
|
|||||||
} from 'react';
|
} from 'react';
|
||||||
|
|
||||||
import type { IChartSerie } from '@openpanel/trpc/src/routers/chart';
|
import type { IChartSerie } from '@openpanel/trpc/src/routers/chart';
|
||||||
import type { IChartInput } from '@openpanel/validation';
|
import type { IChartProps } from '@openpanel/validation';
|
||||||
|
|
||||||
import { ChartLoading } from './ChartLoading';
|
import { ChartLoading } from './ChartLoading';
|
||||||
import { MetricCardLoading } from './MetricCard';
|
import { MetricCardLoading } from './MetricCard';
|
||||||
|
|
||||||
export interface ChartContextType extends IChartInput {
|
export interface ChartContextType extends IChartProps {
|
||||||
editMode?: boolean;
|
editMode?: boolean;
|
||||||
hideID?: boolean;
|
hideID?: boolean;
|
||||||
onClick?: (item: IChartSerie) => void;
|
onClick?: (item: IChartSerie) => void;
|
||||||
|
|||||||
@@ -3,12 +3,11 @@
|
|||||||
import React, { useEffect, useRef } from 'react';
|
import React, { useEffect, useRef } from 'react';
|
||||||
import { useInViewport } from 'react-in-viewport';
|
import { useInViewport } from 'react-in-viewport';
|
||||||
|
|
||||||
import type { ReportChartProps } from '.';
|
|
||||||
import { ChartSwitch } from '.';
|
import { ChartSwitch } from '.';
|
||||||
import { ChartLoading } from './ChartLoading';
|
import { ChartLoading } from './ChartLoading';
|
||||||
import type { ChartContextType } from './ChartProvider';
|
import type { ChartContextType } from './ChartProvider';
|
||||||
|
|
||||||
export function LazyChart(props: ReportChartProps & ChartContextType) {
|
export function LazyChart(props: ChartContextType) {
|
||||||
const ref = useRef<HTMLDivElement>(null);
|
const ref = useRef<HTMLDivElement>(null);
|
||||||
const once = useRef(false);
|
const once = useRef(false);
|
||||||
const { inViewport } = useInViewport(ref, undefined, {
|
const { inViewport } = useInViewport(ref, undefined, {
|
||||||
|
|||||||
@@ -1,15 +1,13 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import type { IChartInput } from '@openpanel/validation';
|
import type { IChartProps } from '@openpanel/validation';
|
||||||
|
|
||||||
import { Funnel } from '../funnel';
|
import { Funnel } from '../funnel';
|
||||||
import { Chart } from './Chart';
|
import { Chart } from './Chart';
|
||||||
import { withChartProivder } from './ChartProvider';
|
import { withChartProivder } from './ChartProvider';
|
||||||
|
|
||||||
export type ReportChartProps = IChartInput;
|
|
||||||
|
|
||||||
export const ChartSwitch = withChartProivder(function ChartSwitch(
|
export const ChartSwitch = withChartProivder(function ChartSwitch(
|
||||||
props: ReportChartProps
|
props: IChartProps
|
||||||
) {
|
) {
|
||||||
if (props.chartType === 'funnel') {
|
if (props.chartType === 'funnel') {
|
||||||
return <Funnel {...props} />;
|
return <Funnel {...props} />;
|
||||||
@@ -19,12 +17,12 @@ export const ChartSwitch = withChartProivder(function ChartSwitch(
|
|||||||
});
|
});
|
||||||
|
|
||||||
interface ChartSwitchShortcutProps {
|
interface ChartSwitchShortcutProps {
|
||||||
projectId: ReportChartProps['projectId'];
|
projectId: IChartProps['projectId'];
|
||||||
range?: ReportChartProps['range'];
|
range?: IChartProps['range'];
|
||||||
previous?: ReportChartProps['previous'];
|
previous?: IChartProps['previous'];
|
||||||
chartType?: ReportChartProps['chartType'];
|
chartType?: IChartProps['chartType'];
|
||||||
interval?: ReportChartProps['interval'];
|
interval?: IChartProps['interval'];
|
||||||
events: ReportChartProps['events'];
|
events: IChartProps['events'];
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ChartSwitchShortcut = ({
|
export const ChartSwitchShortcut = ({
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { AutoSizer } from '@/components/react-virtualized-auto-sizer';
|
|||||||
import { Progress } from '@/components/ui/progress';
|
import { Progress } from '@/components/ui/progress';
|
||||||
import { Widget, WidgetBody } from '@/components/widget';
|
import { Widget, WidgetBody } from '@/components/widget';
|
||||||
import { pushModal } from '@/modals';
|
import { pushModal } from '@/modals';
|
||||||
import { useSelector } from '@/redux';
|
|
||||||
import type { RouterOutputs } from '@/trpc/client';
|
import type { RouterOutputs } from '@/trpc/client';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import { round } from '@/utils/math';
|
import { round } from '@/utils/math';
|
||||||
|
|||||||
@@ -1,38 +1,28 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import type { RouterOutputs } from '@/trpc/client';
|
|
||||||
import { api } from '@/trpc/client';
|
import { api } from '@/trpc/client';
|
||||||
|
|
||||||
import type { IChartInput } from '@openpanel/validation';
|
import type { IChartInput, IChartProps } from '@openpanel/validation';
|
||||||
|
|
||||||
import { ChartEmpty } from '../chart/ChartEmpty';
|
import { ChartEmpty } from '../chart/ChartEmpty';
|
||||||
import { withChartProivder } from '../chart/ChartProvider';
|
import { withChartProivder } from '../chart/ChartProvider';
|
||||||
import { FunnelSteps } from './Funnel';
|
import { FunnelSteps } from './Funnel';
|
||||||
|
|
||||||
export type ReportChartProps = IChartInput & {
|
export type ReportChartProps = IChartProps;
|
||||||
initialData?: RouterOutputs['chart']['funnel'];
|
|
||||||
};
|
|
||||||
|
|
||||||
export const Funnel = withChartProivder(function Chart({
|
export const Funnel = withChartProivder(function Chart({
|
||||||
events,
|
events,
|
||||||
name,
|
|
||||||
range,
|
range,
|
||||||
projectId,
|
projectId,
|
||||||
}: ReportChartProps) {
|
}: ReportChartProps) {
|
||||||
const input: IChartInput = {
|
const input: IChartInput = {
|
||||||
events,
|
events,
|
||||||
name,
|
|
||||||
range,
|
range,
|
||||||
projectId,
|
projectId,
|
||||||
lineType: 'monotone',
|
|
||||||
interval: 'day',
|
interval: 'day',
|
||||||
chartType: 'funnel',
|
chartType: 'funnel',
|
||||||
breakdowns: [],
|
breakdowns: [],
|
||||||
startDate: null,
|
|
||||||
endDate: null,
|
|
||||||
previous: false,
|
previous: false,
|
||||||
formula: undefined,
|
|
||||||
unit: undefined,
|
|
||||||
metric: 'sum',
|
metric: 'sum',
|
||||||
};
|
};
|
||||||
const [data] = api.chart.funnel.useSuspenseQuery(input, {
|
const [data] = api.chart.funnel.useSuspenseQuery(input, {
|
||||||
|
|||||||
@@ -18,14 +18,14 @@ import {
|
|||||||
import type {
|
import type {
|
||||||
IChartBreakdown,
|
IChartBreakdown,
|
||||||
IChartEvent,
|
IChartEvent,
|
||||||
IChartInput,
|
|
||||||
IChartLineType,
|
IChartLineType,
|
||||||
|
IChartProps,
|
||||||
IChartRange,
|
IChartRange,
|
||||||
IChartType,
|
IChartType,
|
||||||
IInterval,
|
IInterval,
|
||||||
} from '@openpanel/validation';
|
} from '@openpanel/validation';
|
||||||
|
|
||||||
type InitialState = IChartInput & {
|
type InitialState = IChartProps & {
|
||||||
dirty: boolean;
|
dirty: boolean;
|
||||||
ready: boolean;
|
ready: boolean;
|
||||||
startDate: string | null;
|
startDate: string | null;
|
||||||
@@ -72,7 +72,7 @@ export const reportSlice = createSlice({
|
|||||||
ready: true,
|
ready: true,
|
||||||
};
|
};
|
||||||
},
|
},
|
||||||
setReport(state, action: PayloadAction<IChartInput>) {
|
setReport(state, action: PayloadAction<IChartProps>) {
|
||||||
return {
|
return {
|
||||||
...state,
|
...state,
|
||||||
...action.payload,
|
...action.payload,
|
||||||
@@ -97,7 +97,7 @@ export const reportSlice = createSlice({
|
|||||||
removeEvent: (
|
removeEvent: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<{
|
action: PayloadAction<{
|
||||||
id: string;
|
id?: string;
|
||||||
}>
|
}>
|
||||||
) => {
|
) => {
|
||||||
state.dirty = true;
|
state.dirty = true;
|
||||||
@@ -135,7 +135,7 @@ export const reportSlice = createSlice({
|
|||||||
removeBreakdown: (
|
removeBreakdown: (
|
||||||
state,
|
state,
|
||||||
action: PayloadAction<{
|
action: PayloadAction<{
|
||||||
id: string;
|
id?: string;
|
||||||
}>
|
}>
|
||||||
) => {
|
) => {
|
||||||
state.dirty = true;
|
state.dirty = true;
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
import { ChartSwitch } from '@/components/report/chart';
|
import { ChartSwitch } from '@/components/report/chart';
|
||||||
import { ScrollArea } from '@/components/ui/scroll-area';
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
||||||
|
|
||||||
import type { IChartInput } from '@openpanel/validation';
|
import type { IChartProps } from '@openpanel/validation';
|
||||||
|
|
||||||
import { ModalContent, ModalHeader } from './Modal/Container';
|
import { ModalContent, ModalHeader } from './Modal/Container';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
chart: IChartInput;
|
chart: IChartProps;
|
||||||
};
|
};
|
||||||
|
|
||||||
const OverviewChartDetails = (props: Props) => {
|
const OverviewChartDetails = (props: Props) => {
|
||||||
|
|||||||
@@ -11,13 +11,13 @@ import { Controller, useForm } from 'react-hook-form';
|
|||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import type { IChartInput } from '@openpanel/validation';
|
import type { IChartProps } from '@openpanel/validation';
|
||||||
|
|
||||||
import { popModal } from '.';
|
import { popModal } from '.';
|
||||||
import { ModalContent, ModalHeader } from './Modal/Container';
|
import { ModalContent, ModalHeader } from './Modal/Container';
|
||||||
|
|
||||||
type SaveReportProps = {
|
type SaveReportProps = {
|
||||||
report: IChartInput;
|
report: IChartProps;
|
||||||
reportId?: string;
|
reportId?: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -71,8 +71,8 @@
|
|||||||
--accent: 0 0% 15.1%; /* #262626 */
|
--accent: 0 0% 15.1%; /* #262626 */
|
||||||
--accent-foreground: 0 0% 98%; /* #fafafa */
|
--accent-foreground: 0 0% 98%; /* #fafafa */
|
||||||
|
|
||||||
--destructive: 0 0% 30.6%; /* #4e4e4e */
|
--destructive: 0 84.2% 60.2%; /* #F2677D */
|
||||||
--destructive-foreground: 0 0% 98%; /* #fafafa */
|
--destructive-foreground: 0 100% 97.25%; /* #F8F9FB */
|
||||||
|
|
||||||
--border: 0 0% 15.1%; /* #262626 */
|
--border: 0 0% 15.1%; /* #262626 */
|
||||||
--input: 0 0% 15.1%; /* #262626 */
|
--input: 0 0% 15.1%; /* #262626 */
|
||||||
|
|||||||
@@ -2,8 +2,8 @@
|
|||||||
"name": "@openpanel/worker",
|
"name": "@openpanel/worker",
|
||||||
"version": "0.0.1",
|
"version": "0.0.1",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "dotenv -e ../../.env -c -v WATCH=1 tsup",
|
"qweqweq": "dotenv -e ../../.env -c -v WATCH=1 tsup",
|
||||||
"testing": "WORKER_PORT=9999 pnpm dev",
|
"qweqwe": "WORKER_PORT=9999 pnpm dev",
|
||||||
"start": "node dist/index.js",
|
"start": "node dist/index.js",
|
||||||
"build": "rm -rf dist && tsup",
|
"build": "rm -rf dist && tsup",
|
||||||
"lint": "eslint .",
|
"lint": "eslint .",
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import type {
|
|||||||
IChartBreakdown,
|
IChartBreakdown,
|
||||||
IChartEvent,
|
IChartEvent,
|
||||||
IChartEventFilter,
|
IChartEventFilter,
|
||||||
IChartInput,
|
|
||||||
IChartLineType,
|
IChartLineType,
|
||||||
|
IChartProps,
|
||||||
IChartRange,
|
IChartRange,
|
||||||
} from '@openpanel/validation';
|
} from '@openpanel/validation';
|
||||||
|
|
||||||
@@ -46,7 +46,7 @@ export function transformReportEvent(
|
|||||||
|
|
||||||
export function transformReport(
|
export function transformReport(
|
||||||
report: DbReport
|
report: DbReport
|
||||||
): IChartInput & { id: string } {
|
): IChartProps & { id: string } {
|
||||||
return {
|
return {
|
||||||
id: report.id,
|
id: report.id,
|
||||||
projectId: report.projectId,
|
projectId: report.projectId,
|
||||||
|
|||||||
@@ -66,6 +66,38 @@ export const organizationRouter = createTRPCRouter({
|
|||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
|
||||||
|
removeMember: protectedProcedure
|
||||||
|
.input(
|
||||||
|
z.object({
|
||||||
|
organizationId: z.string(),
|
||||||
|
userId: z.string(),
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.mutation(async ({ input, ctx }) => {
|
||||||
|
if (ctx.session.userId === input.userId) {
|
||||||
|
throw new Error('You cannot remove yourself from the organization');
|
||||||
|
}
|
||||||
|
const organization = await clerkClient.organizations.getOrganization({
|
||||||
|
organizationId: input.organizationId,
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!organization?.slug) {
|
||||||
|
throw new Error('Organization not found');
|
||||||
|
}
|
||||||
|
|
||||||
|
await db.projectAccess.deleteMany({
|
||||||
|
where: {
|
||||||
|
userId: input.userId,
|
||||||
|
organizationSlug: organization.slug,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
return clerkClient.organizations.deleteOrganizationMembership({
|
||||||
|
organizationId: input.organizationId,
|
||||||
|
userId: input.userId,
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
|
||||||
updateMemberAccess: protectedProcedure
|
updateMemberAccess: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { db } from '@openpanel/db';
|
import { db } from '@openpanel/db';
|
||||||
import { zChartInput } from '@openpanel/validation';
|
import { zReportInput } from '@openpanel/validation';
|
||||||
|
|
||||||
import { createTRPCRouter, protectedProcedure } from '../trpc';
|
import { createTRPCRouter, protectedProcedure } from '../trpc';
|
||||||
|
|
||||||
@@ -9,7 +9,7 @@ export const reportRouter = createTRPCRouter({
|
|||||||
create: protectedProcedure
|
create: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
report: zChartInput.omit({ projectId: true }),
|
report: zReportInput.omit({ projectId: true }),
|
||||||
dashboardId: z.string(),
|
dashboardId: z.string(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
@@ -38,7 +38,7 @@ export const reportRouter = createTRPCRouter({
|
|||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
reportId: z.string(),
|
reportId: z.string(),
|
||||||
report: zChartInput.omit({ projectId: true }),
|
report: zReportInput.omit({ projectId: true }),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.mutation(({ input: { report, reportId } }) => {
|
.mutation(({ input: { report, reportId } }) => {
|
||||||
|
|||||||
@@ -60,9 +60,7 @@ export const zMetric = z.enum(objectToZodEnums(metrics));
|
|||||||
export const zRange = z.enum(objectToZodEnums(timeWindows));
|
export const zRange = z.enum(objectToZodEnums(timeWindows));
|
||||||
|
|
||||||
export const zChartInput = z.object({
|
export const zChartInput = z.object({
|
||||||
name: z.string().default(''),
|
|
||||||
chartType: zChartType.default('linear'),
|
chartType: zChartType.default('linear'),
|
||||||
lineType: zLineType.default('monotone'),
|
|
||||||
interval: zTimeInterval.default('day'),
|
interval: zTimeInterval.default('day'),
|
||||||
events: zChartEvents,
|
events: zChartEvents,
|
||||||
breakdowns: zChartBreakdowns.default([]),
|
breakdowns: zChartBreakdowns.default([]),
|
||||||
@@ -70,13 +68,17 @@ export const zChartInput = z.object({
|
|||||||
previous: z.boolean().default(false),
|
previous: z.boolean().default(false),
|
||||||
formula: z.string().optional(),
|
formula: z.string().optional(),
|
||||||
metric: zMetric.default('sum'),
|
metric: zMetric.default('sum'),
|
||||||
unit: z.string().optional(),
|
|
||||||
previousIndicatorInverted: z.boolean().optional(),
|
|
||||||
projectId: z.string(),
|
projectId: z.string(),
|
||||||
startDate: z.string().nullish(),
|
startDate: z.string().nullish(),
|
||||||
endDate: z.string().nullish(),
|
endDate: z.string().nullish(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const zReportInput = zChartInput.extend({
|
||||||
|
name: z.string(),
|
||||||
|
lineType: zLineType,
|
||||||
|
unit: z.string().optional(),
|
||||||
|
});
|
||||||
|
|
||||||
export const zInviteUser = z.object({
|
export const zInviteUser = z.object({
|
||||||
email: z.string().email(),
|
email: z.string().email(),
|
||||||
organizationSlug: z.string(),
|
organizationSlug: z.string(),
|
||||||
|
|||||||
@@ -8,10 +8,17 @@ import type {
|
|||||||
zLineType,
|
zLineType,
|
||||||
zMetric,
|
zMetric,
|
||||||
zRange,
|
zRange,
|
||||||
|
zReportInput,
|
||||||
zTimeInterval,
|
zTimeInterval,
|
||||||
} from './index';
|
} from './index';
|
||||||
|
|
||||||
export type IChartInput = z.infer<typeof zChartInput>;
|
export type IChartInput = z.infer<typeof zChartInput>;
|
||||||
|
export type IChartProps = z.infer<typeof zReportInput> & {
|
||||||
|
name: string;
|
||||||
|
lineType: IChartLineType;
|
||||||
|
unit?: string;
|
||||||
|
previousIndicatorInverted?: boolean;
|
||||||
|
};
|
||||||
export type IChartEvent = z.infer<typeof zChartEvent>;
|
export type IChartEvent = z.infer<typeof zChartEvent>;
|
||||||
export type IChartEventFilter = IChartEvent['filters'][number];
|
export type IChartEventFilter = IChartEvent['filters'][number];
|
||||||
export type IChartEventFilterValue =
|
export type IChartEventFilterValue =
|
||||||
|
|||||||
Reference in New Issue
Block a user