revoke invites and remove users from organizations
This commit is contained in:
@@ -151,9 +151,7 @@ export async function charts(
|
||||
|
||||
return getChart({
|
||||
...query.data,
|
||||
name: 'export-api',
|
||||
metric: 'sum',
|
||||
lineType: 'monotone',
|
||||
chartType: 'linear',
|
||||
metric: 'sum',
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
</>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ export function CreateClientSuccess({ id, secret, cors }: Props) {
|
||||
<div className="grid gap-4">
|
||||
<button className="text-left" onClick={() => clipboard(id)}>
|
||||
<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}
|
||||
<Copy size={16} />
|
||||
</div>
|
||||
@@ -25,7 +25,7 @@ export function CreateClientSuccess({ id, secret, cors }: Props) {
|
||||
onClick={() => clipboard(secret)}
|
||||
>
|
||||
<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}
|
||||
<Copy size={16} />
|
||||
</div>
|
||||
@@ -40,7 +40,7 @@ export function CreateClientSuccess({ id, secret, cors }: Props) {
|
||||
{cors && (
|
||||
<div className="text-left">
|
||||
<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}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
import { pushModal } from '@/modals';
|
||||
import { ScanEyeIcon } from 'lucide-react';
|
||||
|
||||
import type { IChartInput } from '@openpanel/validation';
|
||||
import type { IChartProps } from '@openpanel/validation';
|
||||
|
||||
type Props = {
|
||||
chart: IChartInput;
|
||||
chart: IChartProps;
|
||||
};
|
||||
|
||||
const OverviewDetailsButton = ({ chart }: Props) => {
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
import { api } from '@/trpc/client';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import type { IChartInput } from '@openpanel/validation';
|
||||
import type { IChartProps } from '@openpanel/validation';
|
||||
|
||||
import { Tooltip, TooltipContent, TooltipTrigger } from '../ui/tooltip';
|
||||
|
||||
@@ -14,7 +14,7 @@ interface OverviewLiveHistogramProps {
|
||||
export function OverviewLiveHistogram({
|
||||
projectId,
|
||||
}: OverviewLiveHistogramProps) {
|
||||
const report: IChartInput = {
|
||||
const report: IChartProps = {
|
||||
projectId,
|
||||
events: [
|
||||
{
|
||||
@@ -41,7 +41,7 @@ export function OverviewLiveHistogram({
|
||||
lineType: 'monotone',
|
||||
previous: false,
|
||||
};
|
||||
const countReport: IChartInput = {
|
||||
const countReport: IChartProps = {
|
||||
name: '',
|
||||
projectId,
|
||||
events: [
|
||||
@@ -81,7 +81,7 @@ export function OverviewLiveHistogram({
|
||||
{staticArray.map((percent, i) => (
|
||||
<div
|
||||
key={i}
|
||||
className="bg-def-200 flex-1 animate-pulse rounded"
|
||||
className="flex-1 animate-pulse rounded bg-def-200"
|
||||
style={{ height: `${percent}%` }}
|
||||
/>
|
||||
))}
|
||||
|
||||
@@ -5,7 +5,7 @@ import { ChartSwitch } from '@/components/report/chart';
|
||||
import { useEventQueryFilters } from '@/hooks/useEventQueryFilters';
|
||||
import { cn } from '@/utils/cn';
|
||||
|
||||
import type { IChartInput } from '@openpanel/validation';
|
||||
import type { IChartProps } from '@openpanel/validation';
|
||||
|
||||
import { OverviewLiveHistogram } from './overview-live-histogram';
|
||||
|
||||
@@ -186,7 +186,7 @@ export default function OverviewMetrics({ projectId }: OverviewMetricsProps) {
|
||||
metric: 'average',
|
||||
unit: 'min',
|
||||
},
|
||||
] satisfies (IChartInput & { id: string })[];
|
||||
] satisfies (IChartProps & { id: string })[];
|
||||
|
||||
const selectedMetric = reports[metric]!;
|
||||
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { parseAsStringEnum, useQueryState } from 'nuqs';
|
||||
|
||||
import { mapKeys } from '@openpanel/validation';
|
||||
import type { IChartInput } from '@openpanel/validation';
|
||||
import type { IChartProps } from '@openpanel/validation';
|
||||
|
||||
export function useOverviewWidget<T extends string>(
|
||||
key: string,
|
||||
widgets: Record<
|
||||
T,
|
||||
{ title: string; btn: string; chart: IChartInput; hide?: boolean }
|
||||
{ title: string; btn: string; chart: IChartProps; hide?: boolean }
|
||||
>
|
||||
) {
|
||||
const keys = Object.keys(widgets) as T[];
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
import { api } from '@/trpc/client';
|
||||
|
||||
import type { IChartInput } from '@openpanel/validation';
|
||||
import type { IChartProps } from '@openpanel/validation';
|
||||
|
||||
import { ChartEmpty } from './ChartEmpty';
|
||||
import { ReportAreaChart } from './ReportAreaChart';
|
||||
@@ -13,7 +13,7 @@ import { ReportMapChart } from './ReportMapChart';
|
||||
import { ReportMetricChart } from './ReportMetricChart';
|
||||
import { ReportPieChart } from './ReportPieChart';
|
||||
|
||||
export type ReportChartProps = IChartInput;
|
||||
export type ReportChartProps = IChartProps;
|
||||
|
||||
export function Chart({
|
||||
interval,
|
||||
@@ -45,20 +45,16 @@ export function Chart({
|
||||
|
||||
const [data] = api.chart.chart.useSuspenseQuery(
|
||||
{
|
||||
// dont send lineType since it does not need to be sent
|
||||
lineType: 'monotone',
|
||||
interval,
|
||||
chartType,
|
||||
events,
|
||||
breakdowns,
|
||||
name,
|
||||
range,
|
||||
startDate,
|
||||
endDate,
|
||||
projectId,
|
||||
previous,
|
||||
formula,
|
||||
unit,
|
||||
metric,
|
||||
},
|
||||
{
|
||||
|
||||
@@ -11,12 +11,12 @@ import {
|
||||
} from 'react';
|
||||
|
||||
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 { MetricCardLoading } from './MetricCard';
|
||||
|
||||
export interface ChartContextType extends IChartInput {
|
||||
export interface ChartContextType extends IChartProps {
|
||||
editMode?: boolean;
|
||||
hideID?: boolean;
|
||||
onClick?: (item: IChartSerie) => void;
|
||||
|
||||
@@ -3,12 +3,11 @@
|
||||
import React, { useEffect, useRef } from 'react';
|
||||
import { useInViewport } from 'react-in-viewport';
|
||||
|
||||
import type { ReportChartProps } from '.';
|
||||
import { ChartSwitch } from '.';
|
||||
import { ChartLoading } from './ChartLoading';
|
||||
import type { ChartContextType } from './ChartProvider';
|
||||
|
||||
export function LazyChart(props: ReportChartProps & ChartContextType) {
|
||||
export function LazyChart(props: ChartContextType) {
|
||||
const ref = useRef<HTMLDivElement>(null);
|
||||
const once = useRef(false);
|
||||
const { inViewport } = useInViewport(ref, undefined, {
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
'use client';
|
||||
|
||||
import type { IChartInput } from '@openpanel/validation';
|
||||
import type { IChartProps } from '@openpanel/validation';
|
||||
|
||||
import { Funnel } from '../funnel';
|
||||
import { Chart } from './Chart';
|
||||
import { withChartProivder } from './ChartProvider';
|
||||
|
||||
export type ReportChartProps = IChartInput;
|
||||
|
||||
export const ChartSwitch = withChartProivder(function ChartSwitch(
|
||||
props: ReportChartProps
|
||||
props: IChartProps
|
||||
) {
|
||||
if (props.chartType === 'funnel') {
|
||||
return <Funnel {...props} />;
|
||||
@@ -19,12 +17,12 @@ export const ChartSwitch = withChartProivder(function ChartSwitch(
|
||||
});
|
||||
|
||||
interface ChartSwitchShortcutProps {
|
||||
projectId: ReportChartProps['projectId'];
|
||||
range?: ReportChartProps['range'];
|
||||
previous?: ReportChartProps['previous'];
|
||||
chartType?: ReportChartProps['chartType'];
|
||||
interval?: ReportChartProps['interval'];
|
||||
events: ReportChartProps['events'];
|
||||
projectId: IChartProps['projectId'];
|
||||
range?: IChartProps['range'];
|
||||
previous?: IChartProps['previous'];
|
||||
chartType?: IChartProps['chartType'];
|
||||
interval?: IChartProps['interval'];
|
||||
events: IChartProps['events'];
|
||||
}
|
||||
|
||||
export const ChartSwitchShortcut = ({
|
||||
|
||||
@@ -5,7 +5,6 @@ import { AutoSizer } from '@/components/react-virtualized-auto-sizer';
|
||||
import { Progress } from '@/components/ui/progress';
|
||||
import { Widget, WidgetBody } from '@/components/widget';
|
||||
import { pushModal } from '@/modals';
|
||||
import { useSelector } from '@/redux';
|
||||
import type { RouterOutputs } from '@/trpc/client';
|
||||
import { cn } from '@/utils/cn';
|
||||
import { round } from '@/utils/math';
|
||||
|
||||
@@ -1,38 +1,28 @@
|
||||
'use client';
|
||||
|
||||
import type { RouterOutputs } 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 { withChartProivder } from '../chart/ChartProvider';
|
||||
import { FunnelSteps } from './Funnel';
|
||||
|
||||
export type ReportChartProps = IChartInput & {
|
||||
initialData?: RouterOutputs['chart']['funnel'];
|
||||
};
|
||||
export type ReportChartProps = IChartProps;
|
||||
|
||||
export const Funnel = withChartProivder(function Chart({
|
||||
events,
|
||||
name,
|
||||
range,
|
||||
projectId,
|
||||
}: ReportChartProps) {
|
||||
const input: IChartInput = {
|
||||
events,
|
||||
name,
|
||||
range,
|
||||
projectId,
|
||||
lineType: 'monotone',
|
||||
interval: 'day',
|
||||
chartType: 'funnel',
|
||||
breakdowns: [],
|
||||
startDate: null,
|
||||
endDate: null,
|
||||
previous: false,
|
||||
formula: undefined,
|
||||
unit: undefined,
|
||||
metric: 'sum',
|
||||
};
|
||||
const [data] = api.chart.funnel.useSuspenseQuery(input, {
|
||||
|
||||
@@ -18,14 +18,14 @@ import {
|
||||
import type {
|
||||
IChartBreakdown,
|
||||
IChartEvent,
|
||||
IChartInput,
|
||||
IChartLineType,
|
||||
IChartProps,
|
||||
IChartRange,
|
||||
IChartType,
|
||||
IInterval,
|
||||
} from '@openpanel/validation';
|
||||
|
||||
type InitialState = IChartInput & {
|
||||
type InitialState = IChartProps & {
|
||||
dirty: boolean;
|
||||
ready: boolean;
|
||||
startDate: string | null;
|
||||
@@ -72,7 +72,7 @@ export const reportSlice = createSlice({
|
||||
ready: true,
|
||||
};
|
||||
},
|
||||
setReport(state, action: PayloadAction<IChartInput>) {
|
||||
setReport(state, action: PayloadAction<IChartProps>) {
|
||||
return {
|
||||
...state,
|
||||
...action.payload,
|
||||
@@ -97,7 +97,7 @@ export const reportSlice = createSlice({
|
||||
removeEvent: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
id: string;
|
||||
id?: string;
|
||||
}>
|
||||
) => {
|
||||
state.dirty = true;
|
||||
@@ -135,7 +135,7 @@ export const reportSlice = createSlice({
|
||||
removeBreakdown: (
|
||||
state,
|
||||
action: PayloadAction<{
|
||||
id: string;
|
||||
id?: string;
|
||||
}>
|
||||
) => {
|
||||
state.dirty = true;
|
||||
|
||||
@@ -1,12 +1,12 @@
|
||||
import { ChartSwitch } from '@/components/report/chart';
|
||||
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';
|
||||
|
||||
type Props = {
|
||||
chart: IChartInput;
|
||||
chart: IChartProps;
|
||||
};
|
||||
|
||||
const OverviewChartDetails = (props: Props) => {
|
||||
|
||||
@@ -11,13 +11,13 @@ import { Controller, useForm } from 'react-hook-form';
|
||||
import { toast } from 'sonner';
|
||||
import { z } from 'zod';
|
||||
|
||||
import type { IChartInput } from '@openpanel/validation';
|
||||
import type { IChartProps } from '@openpanel/validation';
|
||||
|
||||
import { popModal } from '.';
|
||||
import { ModalContent, ModalHeader } from './Modal/Container';
|
||||
|
||||
type SaveReportProps = {
|
||||
report: IChartInput;
|
||||
report: IChartProps;
|
||||
reportId?: string;
|
||||
};
|
||||
|
||||
|
||||
@@ -71,8 +71,8 @@
|
||||
--accent: 0 0% 15.1%; /* #262626 */
|
||||
--accent-foreground: 0 0% 98%; /* #fafafa */
|
||||
|
||||
--destructive: 0 0% 30.6%; /* #4e4e4e */
|
||||
--destructive-foreground: 0 0% 98%; /* #fafafa */
|
||||
--destructive: 0 84.2% 60.2%; /* #F2677D */
|
||||
--destructive-foreground: 0 100% 97.25%; /* #F8F9FB */
|
||||
|
||||
--border: 0 0% 15.1%; /* #262626 */
|
||||
--input: 0 0% 15.1%; /* #262626 */
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
"name": "@openpanel/worker",
|
||||
"version": "0.0.1",
|
||||
"scripts": {
|
||||
"dev": "dotenv -e ../../.env -c -v WATCH=1 tsup",
|
||||
"testing": "WORKER_PORT=9999 pnpm dev",
|
||||
"qweqweq": "dotenv -e ../../.env -c -v WATCH=1 tsup",
|
||||
"qweqwe": "WORKER_PORT=9999 pnpm dev",
|
||||
"start": "node dist/index.js",
|
||||
"build": "rm -rf dist && tsup",
|
||||
"lint": "eslint .",
|
||||
@@ -48,4 +48,4 @@
|
||||
]
|
||||
},
|
||||
"prettier": "@openpanel/prettier-config"
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,8 @@ import type {
|
||||
IChartBreakdown,
|
||||
IChartEvent,
|
||||
IChartEventFilter,
|
||||
IChartInput,
|
||||
IChartLineType,
|
||||
IChartProps,
|
||||
IChartRange,
|
||||
} from '@openpanel/validation';
|
||||
|
||||
@@ -46,7 +46,7 @@ export function transformReportEvent(
|
||||
|
||||
export function transformReport(
|
||||
report: DbReport
|
||||
): IChartInput & { id: string } {
|
||||
): IChartProps & { id: string } {
|
||||
return {
|
||||
id: report.id,
|
||||
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
|
||||
.input(
|
||||
z.object({
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import { z } from 'zod';
|
||||
|
||||
import { db } from '@openpanel/db';
|
||||
import { zChartInput } from '@openpanel/validation';
|
||||
import { zReportInput } from '@openpanel/validation';
|
||||
|
||||
import { createTRPCRouter, protectedProcedure } from '../trpc';
|
||||
|
||||
@@ -9,7 +9,7 @@ export const reportRouter = createTRPCRouter({
|
||||
create: protectedProcedure
|
||||
.input(
|
||||
z.object({
|
||||
report: zChartInput.omit({ projectId: true }),
|
||||
report: zReportInput.omit({ projectId: true }),
|
||||
dashboardId: z.string(),
|
||||
})
|
||||
)
|
||||
@@ -38,7 +38,7 @@ export const reportRouter = createTRPCRouter({
|
||||
.input(
|
||||
z.object({
|
||||
reportId: z.string(),
|
||||
report: zChartInput.omit({ projectId: true }),
|
||||
report: zReportInput.omit({ projectId: true }),
|
||||
})
|
||||
)
|
||||
.mutation(({ input: { report, reportId } }) => {
|
||||
|
||||
@@ -60,9 +60,7 @@ export const zMetric = z.enum(objectToZodEnums(metrics));
|
||||
export const zRange = z.enum(objectToZodEnums(timeWindows));
|
||||
|
||||
export const zChartInput = z.object({
|
||||
name: z.string().default(''),
|
||||
chartType: zChartType.default('linear'),
|
||||
lineType: zLineType.default('monotone'),
|
||||
interval: zTimeInterval.default('day'),
|
||||
events: zChartEvents,
|
||||
breakdowns: zChartBreakdowns.default([]),
|
||||
@@ -70,13 +68,17 @@ export const zChartInput = z.object({
|
||||
previous: z.boolean().default(false),
|
||||
formula: z.string().optional(),
|
||||
metric: zMetric.default('sum'),
|
||||
unit: z.string().optional(),
|
||||
previousIndicatorInverted: z.boolean().optional(),
|
||||
projectId: z.string(),
|
||||
startDate: 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({
|
||||
email: z.string().email(),
|
||||
organizationSlug: z.string(),
|
||||
|
||||
@@ -8,10 +8,17 @@ import type {
|
||||
zLineType,
|
||||
zMetric,
|
||||
zRange,
|
||||
zReportInput,
|
||||
zTimeInterval,
|
||||
} from './index';
|
||||
|
||||
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 IChartEventFilter = IChartEvent['filters'][number];
|
||||
export type IChartEventFilterValue =
|
||||
|
||||
Reference in New Issue
Block a user