fix(dashboard+api): add cors + domain from onboarding

This commit is contained in:
Carl-Gerhard Lindesvärd
2024-12-11 23:35:11 +01:00
parent 5d7bb48b4e
commit cdd13778de
8 changed files with 51 additions and 183 deletions

View File

@@ -50,7 +50,8 @@ const Tracking = ({
defaultValues: { defaultValues: {
organization: '', organization: '',
project: '', project: '',
domain: null, domain: '',
cors: [],
website: false, website: false,
app: false, app: false,
backend: false, backend: false,
@@ -75,6 +76,7 @@ const Tracking = ({
useEffect(() => { useEffect(() => {
if (!isWebsite) { if (!isWebsite) {
form.setValue('domain', null); form.setValue('domain', null);
form.setValue('cors', []);
} }
}, [isWebsite, form]); }, [isWebsite, form]);
@@ -156,36 +158,53 @@ const Tracking = ({
> >
<AnimateHeight open={isWebsite && !isApp}> <AnimateHeight open={isWebsite && !isApp}>
<div className="p-4 pl-14"> <div className="p-4 pl-14">
<InputWithLabel
label="Domain"
placeholder="Your website address"
{...form.register('domain')}
className="mb-4"
error={form.formState.errors.domain?.message}
onBlur={(e) => {
const value = e.target.value.trim();
if (
value.includes('.') &&
form.getValues().cors.length === 0 &&
!form.formState.errors.domain
) {
form.setValue('cors', [value]);
}
}}
/>
<Controller <Controller
name="domain" name="cors"
control={form.control} control={form.control}
render={({ field }) => ( render={({ field }) => (
<WithLabel <WithLabel label="Cors">
label="Domain(s)"
error={form.formState.errors.domain?.message}
>
<TagInput <TagInput
{...field} {...field}
placeholder="Add a domain" id="Cors"
value={field.value?.split(',') ?? []} error={form.formState.errors.cors?.message}
placeholder="Accept events from these domains"
value={field.value ?? []}
renderTag={(tag) => renderTag={(tag) =>
tag === '*' ? 'Allow all domains' : tag tag === '*'
? 'Accept events from any domains'
: tag
} }
onChange={(newValue) => { onChange={(newValue) => {
field.onChange( field.onChange(
newValue newValue.map((item) => {
.map((item) => { const trimmed = item.trim();
const trimmed = item.trim(); if (
if ( trimmed.startsWith('http://') ||
trimmed.startsWith('http://') || trimmed.startsWith('https://') ||
trimmed.startsWith('https://') || trimmed === '*'
trimmed === '*' ) {
) { return trimmed;
return trimmed; }
} return `https://${trimmed}`;
return `https://${trimmed}`; }),
})
.join(','),
); );
}} }}
/> />

View File

@@ -1,56 +0,0 @@
'use client';
import { pushModal, showConfirm } from '@/modals';
import { api } from '@/trpc/client';
import { Edit2Icon, TrashIcon } from 'lucide-react';
import { useRouter } from 'next/navigation';
import { toast } from 'sonner';
import type { IServiceProject } from '@openpanel/db';
import { Button } from '../ui/button';
export function ProjectActions(project: Exclude<IServiceProject, null>) {
const { id } = project;
const router = useRouter();
const deletion = api.project.remove.useMutation({
onSuccess() {
toast('Success', {
description: 'Project deleted successfully.',
});
router.refresh();
},
});
return (
<div className="flex gap-2">
<Button
variant="secondary"
onClick={() => {
pushModal('EditProject', project);
}}
icon={Edit2Icon}
>
Edit project
</Button>
<Button
variant="secondary"
className="text-destructive"
onClick={() => {
showConfirm({
title: 'Delete project',
text: 'This will delete all events for this project. This action cannot be undone.',
onConfirm() {
deletion.mutate({
id,
});
},
});
}}
icon={TrashIcon}
>
Delete project
</Button>
</div>
);
}

View File

@@ -1,28 +0,0 @@
import { formatDate } from '@/utils/date';
import type { ColumnDef } from '@tanstack/react-table';
import type { IServiceProject } from '@openpanel/db';
import { ACTIONS } from '../data-table';
import { ProjectActions } from './project-actions';
export type Project = IServiceProject;
export const columns: ColumnDef<IServiceProject>[] = [
{
accessorKey: 'name',
header: 'Name',
},
{
accessorKey: 'createdAt',
header: 'Created at',
cell({ row }) {
const date = row.original.createdAt;
return <div>{formatDate(date)}</div>;
},
},
{
id: ACTIONS,
header: 'Actions',
cell: ({ row }) => <ProjectActions {...row.original} />,
},
];

View File

@@ -1,72 +0,0 @@
import { ButtonContainer } from '@/components/button-container';
import { InputWithLabel } from '@/components/forms/input-with-label';
import { Button } from '@/components/ui/button';
import { api, handleError } from '@/trpc/client';
import { zodResolver } from '@hookform/resolvers/zod';
import { useRouter } from 'next/navigation';
import { useForm } from 'react-hook-form';
import { toast } from 'sonner';
import { z } from 'zod';
import type { IServiceProject } from '@openpanel/db';
import { popModal } from '.';
import { ModalContent, ModalHeader } from './Modal/Container';
type EditProjectProps = Exclude<IServiceProject, null>;
const validator = z.object({
id: z.string().min(1),
name: z.string().min(1),
});
type IForm = z.infer<typeof validator>;
export default function EditProject({ id, name }: EditProjectProps) {
const router = useRouter();
const { register, handleSubmit, reset, formState } = useForm<IForm>({
resolver: zodResolver(validator),
defaultValues: {
id,
name,
},
});
const mutation = api.project.update.useMutation({
onError: handleError,
onSuccess() {
reset();
router.refresh();
toast('Success', {
description: 'Project updated.',
});
popModal();
},
});
return (
<ModalContent>
<ModalHeader title="Edit project" />
<form
onSubmit={handleSubmit((values) => {
mutation.mutate(values);
})}
>
<InputWithLabel
label="Name"
placeholder="Name"
{...register('name')}
defaultValue={name}
/>
<ButtonContainer>
<Button type="button" variant="outline" onClick={() => popModal()}>
Cancel
</Button>
<Button type="submit" disabled={!formState.isDirty}>
Save
</Button>
</ButtonContainer>
</form>
</ModalContent>
);
}

View File

@@ -20,9 +20,6 @@ const modals = {
EventDetails: dynamic(() => import('./event-details'), { EventDetails: dynamic(() => import('./event-details'), {
loading: Loading, loading: Loading,
}), }),
EditProject: dynamic(() => import('./EditProject'), {
loading: Loading,
}),
EditClient: dynamic(() => import('./EditClient'), { EditClient: dynamic(() => import('./EditClient'), {
loading: Loading, loading: Loading,
}), }),

View File

@@ -88,12 +88,18 @@ export const onboardingRouter = createTRPCRouter({
}); });
} }
if (input.cors.length === 0 && input.website) {
input.cors.push('*');
}
const project = await db.project.create({ const project = await db.project.create({
data: { data: {
id: await getId('project', input.project), id: await getId('project', input.project),
name: input.project, name: input.project,
organizationId: organization.id, organizationId: organization.id,
types, types,
domain: input.domain ? stripTrailingSlash(input.domain) : null,
cors: input.cors.map((c) => stripTrailingSlash(c)),
}, },
}); });

View File

@@ -9,6 +9,7 @@ import {
getProjectsByOrganizationId, getProjectsByOrganizationId,
} from '@openpanel/db'; } from '@openpanel/db';
import { stripTrailingSlash } from '@openpanel/common';
import { zProject } from '@openpanel/validation'; import { zProject } from '@openpanel/validation';
import { getProjectAccess } from '../access'; import { getProjectAccess } from '../access';
import { TRPCAccessError } from '../errors'; import { TRPCAccessError } from '../errors';
@@ -50,8 +51,8 @@ export const projectRouter = createTRPCRouter({
name: input.name, name: input.name,
crossDomain: input.crossDomain, crossDomain: input.crossDomain,
filters: input.filters, filters: input.filters,
cors: input.cors, domain: input.domain ? stripTrailingSlash(input.domain) : null,
domain: input.domain, cors: input.cors?.map((c) => stripTrailingSlash(c)) || [],
}, },
include: { include: {
clients: { clients: {

View File

@@ -113,6 +113,7 @@ export const zOnboardingProject = z
organizationId: z.string().optional(), organizationId: z.string().optional(),
project: z.string().min(3), project: z.string().min(3),
domain: z.string().url().or(z.literal('').or(z.null())), domain: z.string().url().or(z.literal('').or(z.null())),
cors: z.array(z.string()).default([]),
website: z.boolean(), website: z.boolean(),
app: z.boolean(), app: z.boolean(),
backend: z.boolean(), backend: z.boolean(),