fix(dashboard+api): add cors + domain from onboarding
This commit is contained in:
@@ -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(','),
|
|
||||||
);
|
);
|
||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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} />,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
@@ -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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
@@ -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,
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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)),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -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: {
|
||||||
|
|||||||
@@ -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(),
|
||||||
|
|||||||
Reference in New Issue
Block a user