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

@@ -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',
}); });
} }

View File

@@ -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: [

View File

@@ -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}%` }}
/> />
))} ))}

View File

@@ -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}

View File

@@ -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>
); );
} }

View File

@@ -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>
); );
} }

View File

@@ -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>
</>
)}
</> </>
); );
} }

View File

@@ -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>

View File

@@ -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) => {

View File

@@ -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}%` }}
/> />
))} ))}

View File

@@ -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]!;

View File

@@ -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[];

View File

@@ -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,
}, },
{ {

View File

@@ -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;

View File

@@ -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, {

View File

@@ -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 = ({

View File

@@ -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';

View File

@@ -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, {

View File

@@ -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;

View File

@@ -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) => {

View File

@@ -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;
}; };

View File

@@ -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 */

View File

@@ -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 .",

View File

@@ -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,

View File

@@ -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({

View File

@@ -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 } }) => {

View File

@@ -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(),

View File

@@ -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 =