make onboarding + create client easier
This commit is contained in:
@@ -33,6 +33,7 @@
|
|||||||
"@radix-ui/react-progress": "^1.0.3",
|
"@radix-ui/react-progress": "^1.0.3",
|
||||||
"@radix-ui/react-scroll-area": "^1.0.5",
|
"@radix-ui/react-scroll-area": "^1.0.5",
|
||||||
"@radix-ui/react-slot": "^1.0.2",
|
"@radix-ui/react-slot": "^1.0.2",
|
||||||
|
"@radix-ui/react-tabs": "^1.0.4",
|
||||||
"@radix-ui/react-toast": "^1.1.5",
|
"@radix-ui/react-toast": "^1.1.5",
|
||||||
"@radix-ui/react-toggle": "^1.0.3",
|
"@radix-ui/react-toggle": "^1.0.3",
|
||||||
"@radix-ui/react-toggle-group": "^1.0.4",
|
"@radix-ui/react-toggle-group": "^1.0.4",
|
||||||
|
|||||||
@@ -1,252 +0,0 @@
|
|||||||
'use client';
|
|
||||||
|
|
||||||
import { useEffect, useState } from 'react';
|
|
||||||
import { Button } from '@/components/ui/button';
|
|
||||||
import { CheckboxInput } from '@/components/ui/checkbox';
|
|
||||||
import {
|
|
||||||
Dialog,
|
|
||||||
DialogContent,
|
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog';
|
|
||||||
import { Input } from '@/components/ui/input';
|
|
||||||
import { Label } from '@/components/ui/label';
|
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
|
||||||
import { clipboard } from '@/utils/clipboard';
|
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
|
||||||
import { Copy, SaveIcon } from 'lucide-react';
|
|
||||||
import Link from 'next/link';
|
|
||||||
import { useRouter } from 'next/navigation';
|
|
||||||
import type { SubmitHandler } from 'react-hook-form';
|
|
||||||
import { Controller, useForm, useWatch } from 'react-hook-form';
|
|
||||||
import { toast } from 'sonner';
|
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import { api, handleError } from '../../../_trpc/client';
|
|
||||||
|
|
||||||
const validation = z.object({
|
|
||||||
name: z.string().min(1),
|
|
||||||
domain: z.string().optional(),
|
|
||||||
withSecret: z.boolean().optional(),
|
|
||||||
});
|
|
||||||
|
|
||||||
type IForm = z.infer<typeof validation>;
|
|
||||||
|
|
||||||
export function CreateClient() {
|
|
||||||
const [open, setOpen] = useState(false);
|
|
||||||
const { organizationId, projectId } = useAppParams();
|
|
||||||
const clients = api.client.list.useQuery({
|
|
||||||
organizationId,
|
|
||||||
});
|
|
||||||
const clientsCount = clients.data?.length;
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (clientsCount === 0) {
|
|
||||||
setOpen(true);
|
|
||||||
}
|
|
||||||
}, [clientsCount]);
|
|
||||||
|
|
||||||
const router = useRouter();
|
|
||||||
const form = useForm<IForm>({
|
|
||||||
resolver: zodResolver(validation),
|
|
||||||
defaultValues: {
|
|
||||||
withSecret: false,
|
|
||||||
name: '',
|
|
||||||
domain: '',
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const mutation = api.client.create2.useMutation({
|
|
||||||
onError: handleError,
|
|
||||||
onSuccess() {
|
|
||||||
toast.success('Client created');
|
|
||||||
router.refresh();
|
|
||||||
},
|
|
||||||
});
|
|
||||||
const onSubmit: SubmitHandler<IForm> = (values) => {
|
|
||||||
mutation.mutate({
|
|
||||||
name: values.name,
|
|
||||||
domain: values.withSecret ? undefined : values.domain,
|
|
||||||
organizationId,
|
|
||||||
projectId,
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const watch = useWatch({
|
|
||||||
control: form.control,
|
|
||||||
name: 'withSecret',
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Dialog open={open}>
|
|
||||||
{mutation.isSuccess ? (
|
|
||||||
<>
|
|
||||||
<DialogContent
|
|
||||||
className="sm:max-w-[425px]"
|
|
||||||
onClose={() => setOpen(false)}
|
|
||||||
>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Success</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
{mutation.data.clientSecret
|
|
||||||
? 'Use your client id and secret with our SDK to send events to us. '
|
|
||||||
: 'Use your client id with our SDK to send events to us. '}
|
|
||||||
See our{' '}
|
|
||||||
<Link href="https//openpanel.dev/docs" className="underline">
|
|
||||||
documentation
|
|
||||||
</Link>
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<div className="grid gap-4 py-4">
|
|
||||||
<button
|
|
||||||
className="mt-4 text-left"
|
|
||||||
onClick={() => clipboard(mutation.data.clientId)}
|
|
||||||
>
|
|
||||||
<Label>Client ID</Label>
|
|
||||||
<div className="flex items-center justify-between rounded bg-gray-100 p-2 px-3">
|
|
||||||
{mutation.data.clientId}
|
|
||||||
<Copy size={16} />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
{mutation.data.clientSecret ? (
|
|
||||||
<button
|
|
||||||
className="mt-4 text-left"
|
|
||||||
onClick={() => clipboard(mutation.data.clientId)}
|
|
||||||
>
|
|
||||||
<Label>Secret</Label>
|
|
||||||
<div className="flex items-center justify-between rounded bg-gray-100 p-2 px-3">
|
|
||||||
{mutation.data.clientSecret}
|
|
||||||
<Copy size={16} />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<div className="mt-4 text-left">
|
|
||||||
<Label>Cors settings</Label>
|
|
||||||
<div className="flex items-center justify-between rounded bg-gray-100 p-2 px-3">
|
|
||||||
{mutation.data.cors}
|
|
||||||
</div>
|
|
||||||
<div className="text-sm italic mt-1">
|
|
||||||
You can update cors settings{' '}
|
|
||||||
<Link className="underline" href="/qwe/qwe/">
|
|
||||||
here
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant={'secondary'}
|
|
||||||
onClick={() => setOpen(false)}
|
|
||||||
>
|
|
||||||
Close
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</DialogContent>
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
<>
|
|
||||||
<DialogContent
|
|
||||||
className="sm:max-w-[425px]"
|
|
||||||
onClose={() => setOpen(false)}
|
|
||||||
>
|
|
||||||
<DialogHeader>
|
|
||||||
<DialogTitle>Let's connect</DialogTitle>
|
|
||||||
<DialogDescription>
|
|
||||||
Create a client so you can start send events to us 🚀
|
|
||||||
</DialogDescription>
|
|
||||||
</DialogHeader>
|
|
||||||
<form
|
|
||||||
className="flex flex-col gap-4"
|
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
>
|
|
||||||
<div className="grid gap-4 py-4">
|
|
||||||
<div>
|
|
||||||
<Label>Client name</Label>
|
|
||||||
<Input
|
|
||||||
placeholder="Eg. My App Client"
|
|
||||||
error={form.formState.errors.name?.message}
|
|
||||||
{...form.register('name')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Controller
|
|
||||||
name="withSecret"
|
|
||||||
control={form.control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<CheckboxInput
|
|
||||||
defaultChecked={field.value}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
field.onChange(!checked);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
This is a website
|
|
||||||
</CheckboxInput>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<Label>Your domain name</Label>
|
|
||||||
<Input
|
|
||||||
placeholder="https://...."
|
|
||||||
error={form.formState.errors.domain?.message}
|
|
||||||
{...form.register('domain')}
|
|
||||||
disabled={watch}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant={'secondary'}
|
|
||||||
onClick={() => setOpen(false)}
|
|
||||||
>
|
|
||||||
Cancel
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
icon={SaveIcon}
|
|
||||||
loading={mutation.isLoading}
|
|
||||||
>
|
|
||||||
Create
|
|
||||||
</Button>
|
|
||||||
</DialogFooter>
|
|
||||||
</form>
|
|
||||||
</DialogContent>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Dialog>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
// <div>
|
|
||||||
// <div className="text-lg">
|
|
||||||
// Select your framework and we'll generate a client for you.
|
|
||||||
// </div>
|
|
||||||
// <div className="flex flex-wrap gap-2 mt-8">
|
|
||||||
// <FeatureButton
|
|
||||||
// name="React"
|
|
||||||
// logo="https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/2300px-React-icon.svg.png"
|
|
||||||
// />
|
|
||||||
// <FeatureButton
|
|
||||||
// name="React Native"
|
|
||||||
// logo="https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/2300px-React-icon.svg.png"
|
|
||||||
// />
|
|
||||||
// <FeatureButton
|
|
||||||
// name="Next.js"
|
|
||||||
// logo="https://static-00.iconduck.com/assets.00/nextjs-icon-512x512-y563b8iq.png"
|
|
||||||
// />
|
|
||||||
// <FeatureButton
|
|
||||||
// name="Remix"
|
|
||||||
// logo="https://www.datocms-assets.com/205/1642515307-square-logo.svg"
|
|
||||||
// />
|
|
||||||
// <FeatureButton
|
|
||||||
// name="Vue"
|
|
||||||
// logo="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ0UhQnp6TUPCwAr3ruTEwBDiTN5HLAWaoUD3AJIgtepQ&s"
|
|
||||||
// />
|
|
||||||
// <FeatureButton
|
|
||||||
// name="HTML"
|
|
||||||
// logo="https://upload.wikimedia.org/wikipedia/commons/thumb/6/61/HTML5_logo_and_wordmark.svg/240px-HTML5_logo_and_wordmark.svg.png"
|
|
||||||
// />
|
|
||||||
// </div>
|
|
||||||
// </div>
|
|
||||||
@@ -4,11 +4,10 @@ import { useEffect } from 'react';
|
|||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import { cn } from '@/utils/cn';
|
import { cn } from '@/utils/cn';
|
||||||
import { useUser } from '@clerk/nextjs';
|
import { useUser } from '@clerk/nextjs';
|
||||||
import type { IServiceDashboards } from '@openpanel/db';
|
|
||||||
import {
|
import {
|
||||||
|
BookmarkIcon,
|
||||||
BuildingIcon,
|
BuildingIcon,
|
||||||
CogIcon,
|
CogIcon,
|
||||||
DotIcon,
|
|
||||||
GanttChartIcon,
|
GanttChartIcon,
|
||||||
KeySquareIcon,
|
KeySquareIcon,
|
||||||
LayoutPanelTopIcon,
|
LayoutPanelTopIcon,
|
||||||
@@ -21,6 +20,8 @@ import type { LucideProps } from 'lucide-react';
|
|||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { usePathname } from 'next/navigation';
|
import { usePathname } from 'next/navigation';
|
||||||
|
|
||||||
|
import type { IServiceDashboards } from '@openpanel/db';
|
||||||
|
|
||||||
function LinkWithIcon({
|
function LinkWithIcon({
|
||||||
href,
|
href,
|
||||||
icon: Icon,
|
icon: Icon,
|
||||||
@@ -127,7 +128,7 @@ export default function LayoutMenu({ dashboards }: LayoutMenuProps) {
|
|||||||
href={`/${params.organizationId}/${projectId}/settings/profile`}
|
href={`/${params.organizationId}/${projectId}/settings/profile`}
|
||||||
/>
|
/>
|
||||||
<LinkWithIcon
|
<LinkWithIcon
|
||||||
icon={UserIcon}
|
icon={BookmarkIcon}
|
||||||
label="References"
|
label="References"
|
||||||
href={`/${params.organizationId}/${projectId}/settings/references`}
|
href={`/${params.organizationId}/${projectId}/settings/references`}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ import OverviewTopGeo from '@/components/overview/overview-top-geo';
|
|||||||
import OverviewTopPages from '@/components/overview/overview-top-pages';
|
import OverviewTopPages from '@/components/overview/overview-top-pages';
|
||||||
import OverviewTopSources from '@/components/overview/overview-top-sources';
|
import OverviewTopSources from '@/components/overview/overview-top-sources';
|
||||||
import { getExists } from '@/server/pageExists';
|
import { getExists } from '@/server/pageExists';
|
||||||
|
|
||||||
import { db } from '@openpanel/db';
|
import { db } from '@openpanel/db';
|
||||||
|
|
||||||
import OverviewMetrics from '../../../../components/overview/overview-metrics';
|
import OverviewMetrics from '../../../../components/overview/overview-metrics';
|
||||||
import { CreateClient } from './create-client';
|
|
||||||
import { StickyBelowHeader } from './layout-sticky-below-header';
|
import { StickyBelowHeader } from './layout-sticky-below-header';
|
||||||
import { OverviewReportRange } from './overview-sticky-header';
|
import { OverviewReportRange } from './overview-sticky-header';
|
||||||
|
|
||||||
@@ -38,7 +38,6 @@ export default async function Page({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<PageLayout title="Overview" organizationSlug={organizationId}>
|
<PageLayout title="Overview" organizationSlug={organizationId}>
|
||||||
<CreateClient />
|
|
||||||
<StickyBelowHeader>
|
<StickyBelowHeader>
|
||||||
<div className="p-4 flex gap-2 justify-between">
|
<div className="p-4 flex gap-2 justify-between">
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
|
|||||||
@@ -1,19 +1,23 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { buttonVariants } from '@/components/ui/button';
|
||||||
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
|
import { Widget, WidgetBody, WidgetHead } from '@/components/Widget';
|
||||||
import { SignOutButton } from '@clerk/nextjs';
|
import { SignOutButton } from '@clerk/nextjs';
|
||||||
|
|
||||||
export function Logout() {
|
export function Logout() {
|
||||||
return (
|
return (
|
||||||
<Widget className="border-destructive">
|
<Widget className="border-destructive">
|
||||||
<WidgetHead className="border-destructive">
|
<WidgetHead>
|
||||||
<span className="title text-destructive">Sad part</span>
|
<span className="title">Sad part</span>
|
||||||
</WidgetHead>
|
</WidgetHead>
|
||||||
<WidgetBody>
|
<WidgetBody>
|
||||||
<p className="mb-4">
|
<p className="mb-4">
|
||||||
Sometime's you need to go. See you next time
|
Sometime's you need to go. See you next time
|
||||||
</p>
|
</p>
|
||||||
<SignOutButton />
|
<SignOutButton
|
||||||
|
// @ts-expect-error
|
||||||
|
className={buttonVariants({ variant: 'destructive' })}
|
||||||
|
/>
|
||||||
</WidgetBody>
|
</WidgetBody>
|
||||||
</Widget>
|
</Widget>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -2,7 +2,6 @@
|
|||||||
|
|
||||||
import { LogoSquare } from '@/components/Logo';
|
import { LogoSquare } from '@/components/Logo';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Checkbox } from '@/components/ui/checkbox';
|
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
@@ -79,35 +78,3 @@ export function CreateProject() {
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
// <div>
|
|
||||||
// <div className="text-lg">
|
|
||||||
// Select your framework and we'll generate a client for you.
|
|
||||||
// </div>
|
|
||||||
// <div className="flex flex-wrap gap-2 mt-8">
|
|
||||||
// <FeatureButton
|
|
||||||
// name="React"
|
|
||||||
// logo="https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/2300px-React-icon.svg.png"
|
|
||||||
// />
|
|
||||||
// <FeatureButton
|
|
||||||
// name="React Native"
|
|
||||||
// logo="https://upload.wikimedia.org/wikipedia/commons/thumb/a/a7/React-icon.svg/2300px-React-icon.svg.png"
|
|
||||||
// />
|
|
||||||
// <FeatureButton
|
|
||||||
// name="Next.js"
|
|
||||||
// logo="https://static-00.iconduck.com/assets.00/nextjs-icon-512x512-y563b8iq.png"
|
|
||||||
// />
|
|
||||||
// <FeatureButton
|
|
||||||
// name="Remix"
|
|
||||||
// logo="https://www.datocms-assets.com/205/1642515307-square-logo.svg"
|
|
||||||
// />
|
|
||||||
// <FeatureButton
|
|
||||||
// name="Vue"
|
|
||||||
// logo="https://encrypted-tbn0.gstatic.com/images?q=tbn:ANd9GcQ0UhQnp6TUPCwAr3ruTEwBDiTN5HLAWaoUD3AJIgtepQ&s"
|
|
||||||
// />
|
|
||||||
// <FeatureButton
|
|
||||||
// name="HTML"
|
|
||||||
// logo="https://upload.wikimedia.org/wikipedia/commons/thumb/6/61/HTML5_logo_and_wordmark.svg/240px-HTML5_logo_and_wordmark.svg.png"
|
|
||||||
// />
|
|
||||||
// </div>
|
|
||||||
// </div>
|
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export default async function Page({ params: { organizationId } }: PageProps) {
|
|||||||
const isAccepted = await isWaitlistUserAccepted();
|
const isAccepted = await isWaitlistUserAccepted();
|
||||||
if (!isAccepted) {
|
if (!isAccepted) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-screen">
|
<div className="p-4 flex items-center justify-center h-screen">
|
||||||
<div className="max-w-lg w-full">
|
<div className="max-w-lg w-full">
|
||||||
<LogoSquare className="w-20 md:w-28 mb-8" />
|
<LogoSquare className="w-20 md:w-28 mb-8" />
|
||||||
<h1 className="font-medium text-3xl">Not quite there yet</h1>
|
<h1 className="font-medium text-3xl">Not quite there yet</h1>
|
||||||
@@ -46,7 +46,7 @@ export default async function Page({ params: { organizationId } }: PageProps) {
|
|||||||
|
|
||||||
if (projects.length === 0) {
|
if (projects.length === 0) {
|
||||||
return (
|
return (
|
||||||
<div className="flex items-center justify-center h-screen">
|
<div className="flex items-center justify-center h-screen p-4 ">
|
||||||
<div className="max-w-lg w-full">
|
<div className="max-w-lg w-full">
|
||||||
<CreateProject />
|
<CreateProject />
|
||||||
</div>
|
</div>
|
||||||
@@ -59,7 +59,7 @@ export default async function Page({ params: { organizationId } }: PageProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="max-w-xl w-full mx-auto flex flex-col gap-4 pt-20">
|
<div className="max-w-xl w-full mx-auto flex flex-col gap-4 pt-20 p-4 ">
|
||||||
<h1 className="font-medium text-xl">Select project</h1>
|
<h1 className="font-medium text-xl">Select project</h1>
|
||||||
{projects.map((item) => (
|
{projects.map((item) => (
|
||||||
<ProjectCard key={item.id} {...item} />
|
<ProjectCard key={item.id} {...item} />
|
||||||
|
|||||||
@@ -1,11 +1,16 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
|
import { useState } from 'react';
|
||||||
|
import { CreateClientSuccess } from '@/components/clients/create-client-success';
|
||||||
import { LogoSquare } from '@/components/Logo';
|
import { LogoSquare } from '@/components/Logo';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button, buttonVariants } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
|
import { cn } from '@/utils/cn';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { SaveIcon } from 'lucide-react';
|
import { SaveIcon, WallpaperIcon } from 'lucide-react';
|
||||||
|
import Link from 'next/link';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import type { SubmitHandler } from 'react-hook-form';
|
import type { SubmitHandler } from 'react-hook-form';
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from 'react-hook-form';
|
||||||
@@ -13,10 +18,21 @@ import { z } from 'zod';
|
|||||||
|
|
||||||
import { api, handleError } from '../_trpc/client';
|
import { api, handleError } from '../_trpc/client';
|
||||||
|
|
||||||
const validation = z.object({
|
const validation = z
|
||||||
organization: z.string().min(4),
|
.object({
|
||||||
project: z.string().optional(),
|
organization: z.string().min(3),
|
||||||
});
|
project: z.string().min(3),
|
||||||
|
cors: z.string().nullable(),
|
||||||
|
tab: z.string(),
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(data) =>
|
||||||
|
data.tab === 'other' || (data.tab === 'website' && data.cors !== ''),
|
||||||
|
{
|
||||||
|
message: 'Cors is required',
|
||||||
|
path: ['cors'],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
type IForm = z.infer<typeof validation>;
|
type IForm = z.infer<typeof validation>;
|
||||||
|
|
||||||
@@ -24,63 +40,117 @@ export function CreateOrganization() {
|
|||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
const form = useForm<IForm>({
|
const form = useForm<IForm>({
|
||||||
resolver: zodResolver(validation),
|
resolver: zodResolver(validation),
|
||||||
|
defaultValues: {
|
||||||
|
organization: '',
|
||||||
|
project: '',
|
||||||
|
cors: '',
|
||||||
|
tab: 'website',
|
||||||
|
},
|
||||||
});
|
});
|
||||||
const mutation = api.onboarding.organziation.useMutation({
|
const mutation = api.onboarding.organziation.useMutation({
|
||||||
onError: handleError,
|
onError: handleError,
|
||||||
onSuccess({ organization, project }) {
|
|
||||||
let url = `/${organization.slug}`;
|
|
||||||
if (project) {
|
|
||||||
url += `/${project.id}`;
|
|
||||||
}
|
|
||||||
router.replace(url);
|
|
||||||
},
|
|
||||||
});
|
});
|
||||||
const onSubmit: SubmitHandler<IForm> = (values) => {
|
const onSubmit: SubmitHandler<IForm> = (values) => {
|
||||||
mutation.mutate(values);
|
mutation.mutate({
|
||||||
|
...values,
|
||||||
|
cors: values.tab === 'website' ? values.cors : null,
|
||||||
|
});
|
||||||
};
|
};
|
||||||
return (
|
|
||||||
<>
|
if (mutation.isSuccess && mutation.data.client) {
|
||||||
<div>
|
return (
|
||||||
<LogoSquare className="w-20 md:w-28 mb-8" />
|
<div className="card p-4 md:p-8">
|
||||||
<h1 className="font-medium text-3xl">Welcome to Openpanel</h1>
|
<LogoSquare className="w-20 mb-4" />
|
||||||
<div className="text-lg">
|
<h1 className="font-medium text-3xl">Nice job!</h1>
|
||||||
Create your organization below (can be personal or a company) and
|
<div className="mb-4">
|
||||||
optionally your first project 🤠
|
You're ready to start using our SDK. Save the client ID and secret (if
|
||||||
|
you have any)
|
||||||
|
</div>
|
||||||
|
<CreateClientSuccess {...mutation.data.client} />
|
||||||
|
<div className="flex gap-4 mt-4">
|
||||||
|
<a
|
||||||
|
className={cn(buttonVariants({ variant: 'secondary' }), 'flex-1')}
|
||||||
|
href="https://docs.openpanel.dev/docs"
|
||||||
|
target="_blank"
|
||||||
|
>
|
||||||
|
Read docs
|
||||||
|
</a>
|
||||||
|
<Button
|
||||||
|
className="flex-1"
|
||||||
|
onClick={() => router.refresh()}
|
||||||
|
icon={WallpaperIcon}
|
||||||
|
>
|
||||||
|
Dashboard
|
||||||
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
<form
|
|
||||||
className="mt-8 flex flex-col gap-4"
|
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
|
||||||
>
|
|
||||||
<div>
|
|
||||||
<Label>Organization name *</Label>
|
|
||||||
<Input
|
|
||||||
placeholder="Organization name"
|
|
||||||
size="large"
|
|
||||||
error={form.formState.errors.organization?.message}
|
|
||||||
{...form.register('organization')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<Label>Project name</Label>
|
|
||||||
<Input
|
|
||||||
placeholder="Project name"
|
|
||||||
size="large"
|
|
||||||
error={form.formState.errors.project?.message}
|
|
||||||
{...form.register('project')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div className="flex justify-end">
|
|
||||||
<Button
|
|
||||||
type="submit"
|
|
||||||
size="lg"
|
|
||||||
icon={SaveIcon}
|
|
||||||
loading={mutation.isLoading}
|
|
||||||
>
|
|
||||||
Create
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
</div>
|
||||||
</>
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="card p-4 md:p-8">
|
||||||
|
<LogoSquare className="w-20 mb-4" />
|
||||||
|
<h1 className="font-medium text-3xl">Welcome to Openpanel</h1>
|
||||||
|
<div className="text-lg">
|
||||||
|
Create your organization below (can be personal or a company) and your
|
||||||
|
first project.
|
||||||
|
</div>
|
||||||
|
<form
|
||||||
|
className="mt-8 flex flex-col gap-4"
|
||||||
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
|
>
|
||||||
|
<div>
|
||||||
|
<Label>Organization name *</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="Organization name"
|
||||||
|
error={form.formState.errors.organization?.message}
|
||||||
|
{...form.register('organization')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Project name *</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="Project name"
|
||||||
|
error={form.formState.errors.project?.message}
|
||||||
|
{...form.register('project')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Tabs
|
||||||
|
defaultValue="website"
|
||||||
|
onValueChange={(val) => form.setValue('tab', val)}
|
||||||
|
className="h-28"
|
||||||
|
>
|
||||||
|
<TabsList className="bg-slate-200">
|
||||||
|
<TabsTrigger value="website">Website</TabsTrigger>
|
||||||
|
<TabsTrigger value="other">Other</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value="website">
|
||||||
|
<Label>Cors *</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="https://example.com"
|
||||||
|
error={form.formState.errors.cors?.message}
|
||||||
|
{...form.register('cors')}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="other">
|
||||||
|
<div className="p-2 px-3 bg-white rounded text-sm">
|
||||||
|
🔑 You will get a secret to use for your API requests.
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
|
|
||||||
|
<div className="flex justify-end">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
size="lg"
|
||||||
|
icon={SaveIcon}
|
||||||
|
loading={mutation.isLoading}
|
||||||
|
>
|
||||||
|
Create
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import { Alert, AlertDescription, AlertTitle } from '@/components/ui/alert';
|
||||||
|
import { clipboard } from '@/utils/clipboard';
|
||||||
|
import { Copy, RocketIcon } from 'lucide-react';
|
||||||
|
import Link from 'next/link';
|
||||||
|
|
||||||
|
import type { IServiceClient } from '@openpanel/db';
|
||||||
|
|
||||||
|
import { Label } from '../ui/label';
|
||||||
|
|
||||||
|
type Props = IServiceClient;
|
||||||
|
|
||||||
|
export function CreateClientSuccess({ id, secret, cors }: Props) {
|
||||||
|
return (
|
||||||
|
<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">
|
||||||
|
{id}
|
||||||
|
<Copy size={16} />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
{secret ? (
|
||||||
|
<button className="text-left" onClick={() => clipboard(secret)}>
|
||||||
|
<Label>Secret</Label>
|
||||||
|
<div className="flex items-center justify-between rounded bg-gray-100 p-2 px-3">
|
||||||
|
{secret}
|
||||||
|
<Copy size={16} />
|
||||||
|
</div>
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<div className="text-left">
|
||||||
|
<Label>Cors settings</Label>
|
||||||
|
<div className="flex items-center justify-between rounded bg-gray-100 p-2 px-3">
|
||||||
|
{cors}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
<Alert>
|
||||||
|
<RocketIcon className="h-4 w-4" />
|
||||||
|
<AlertTitle>Get started!</AlertTitle>
|
||||||
|
<AlertDescription>
|
||||||
|
Read our documentation to get started. Easy peasy!
|
||||||
|
</AlertDescription>
|
||||||
|
</Alert>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -29,6 +29,8 @@ const Input = React.forwardRef<HTMLInputElement, InputProps>(
|
|||||||
({ className, error, type, size, ...props }, ref) => {
|
({ className, error, type, size, ...props }, ref) => {
|
||||||
return (
|
return (
|
||||||
<input
|
<input
|
||||||
|
autoComplete="off"
|
||||||
|
autoCorrect="off"
|
||||||
type={type}
|
type={type}
|
||||||
className={cn(
|
className={cn(
|
||||||
inputVariant({ size, className }),
|
inputVariant({ size, className }),
|
||||||
|
|||||||
53
apps/dashboard/src/components/ui/tabs.tsx
Normal file
53
apps/dashboard/src/components/ui/tabs.tsx
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
import * as React from "react"
|
||||||
|
import * as TabsPrimitive from "@radix-ui/react-tabs"
|
||||||
|
|
||||||
|
import { cn } from "@/utils/cn"
|
||||||
|
|
||||||
|
const Tabs = TabsPrimitive.Root
|
||||||
|
|
||||||
|
const TabsList = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.List>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.List>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.List
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex h-10 items-center justify-center rounded-md bg-muted p-1 text-muted-foreground",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TabsList.displayName = TabsPrimitive.List.displayName
|
||||||
|
|
||||||
|
const TabsTrigger = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Trigger>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Trigger>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Trigger
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"inline-flex items-center justify-center whitespace-nowrap rounded-sm px-3 py-1.5 text-sm font-medium ring-offset-background transition-all focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50 data-[state=active]:bg-background data-[state=active]:text-foreground data-[state=active]:shadow-sm",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TabsTrigger.displayName = TabsPrimitive.Trigger.displayName
|
||||||
|
|
||||||
|
const TabsContent = React.forwardRef<
|
||||||
|
React.ElementRef<typeof TabsPrimitive.Content>,
|
||||||
|
React.ComponentPropsWithoutRef<typeof TabsPrimitive.Content>
|
||||||
|
>(({ className, ...props }, ref) => (
|
||||||
|
<TabsPrimitive.Content
|
||||||
|
ref={ref}
|
||||||
|
className={cn(
|
||||||
|
"mt-2 ring-offset-background focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2",
|
||||||
|
className
|
||||||
|
)}
|
||||||
|
{...props}
|
||||||
|
/>
|
||||||
|
))
|
||||||
|
TabsContent.displayName = TabsPrimitive.Content.displayName
|
||||||
|
|
||||||
|
export { Tabs, TabsList, TabsTrigger, TabsContent }
|
||||||
@@ -1,38 +1,42 @@
|
|||||||
'use client';
|
'use client';
|
||||||
|
|
||||||
import { useEffect } from 'react';
|
|
||||||
import { api, handleError } from '@/app/_trpc/client';
|
import { api, handleError } from '@/app/_trpc/client';
|
||||||
import { Button } from '@/components/ui/button';
|
import { CreateClientSuccess } from '@/components/clients/create-client-success';
|
||||||
import { CheckboxInput } from '@/components/ui/checkbox';
|
import { Button, buttonVariants } from '@/components/ui/button';
|
||||||
import { Combobox } from '@/components/ui/combobox';
|
import { Combobox } from '@/components/ui/combobox';
|
||||||
import {
|
import { DialogFooter } from '@/components/ui/dialog';
|
||||||
DialogDescription,
|
|
||||||
DialogFooter,
|
|
||||||
DialogHeader,
|
|
||||||
DialogTitle,
|
|
||||||
} from '@/components/ui/dialog';
|
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
||||||
import { useAppParams } from '@/hooks/useAppParams';
|
import { useAppParams } from '@/hooks/useAppParams';
|
||||||
import { clipboard } from '@/utils/clipboard';
|
import { cn } from '@/utils/cn';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { Copy, SaveIcon } from 'lucide-react';
|
import { SaveIcon, WallpaperIcon } from 'lucide-react';
|
||||||
import Link from 'next/link';
|
import Link from 'next/link';
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from 'next/navigation';
|
||||||
import type { SubmitHandler } from 'react-hook-form';
|
import type { SubmitHandler } from 'react-hook-form';
|
||||||
import { Controller, useForm, useWatch } from 'react-hook-form';
|
import { Controller, useForm } from 'react-hook-form';
|
||||||
import { toast } from 'sonner';
|
import { toast } from 'sonner';
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { popModal } from '.';
|
import { popModal } from '.';
|
||||||
import { ModalContent, ModalHeader } from './Modal/Container';
|
import { ModalContent, ModalHeader } from './Modal/Container';
|
||||||
|
|
||||||
const validation = z.object({
|
const validation = z
|
||||||
name: z.string().min(1),
|
.object({
|
||||||
domain: z.string().optional(),
|
name: z.string().min(1),
|
||||||
withSecret: z.boolean().optional(),
|
cors: z.string().nullable(),
|
||||||
projectId: z.string(),
|
tab: z.string(),
|
||||||
});
|
projectId: z.string(),
|
||||||
|
})
|
||||||
|
.refine(
|
||||||
|
(data) =>
|
||||||
|
data.tab === 'other' || (data.tab === 'website' && data.cors !== ''),
|
||||||
|
{
|
||||||
|
message: 'Cors is required',
|
||||||
|
path: ['cors'],
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
type IForm = z.infer<typeof validation>;
|
type IForm = z.infer<typeof validation>;
|
||||||
|
|
||||||
@@ -42,13 +46,13 @@ export default function AddClient() {
|
|||||||
const form = useForm<IForm>({
|
const form = useForm<IForm>({
|
||||||
resolver: zodResolver(validation),
|
resolver: zodResolver(validation),
|
||||||
defaultValues: {
|
defaultValues: {
|
||||||
withSecret: false,
|
|
||||||
name: '',
|
name: '',
|
||||||
domain: '',
|
cors: '',
|
||||||
|
tab: 'website',
|
||||||
projectId,
|
projectId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
const mutation = api.client.create2.useMutation({
|
const mutation = api.client.create.useMutation({
|
||||||
onError: handleError,
|
onError: handleError,
|
||||||
onSuccess() {
|
onSuccess() {
|
||||||
toast.success('Client created');
|
toast.success('Client created');
|
||||||
@@ -61,81 +65,30 @@ export default function AddClient() {
|
|||||||
const onSubmit: SubmitHandler<IForm> = (values) => {
|
const onSubmit: SubmitHandler<IForm> = (values) => {
|
||||||
mutation.mutate({
|
mutation.mutate({
|
||||||
name: values.name,
|
name: values.name,
|
||||||
domain: values.withSecret ? undefined : values.domain,
|
cors: values.tab === 'website' ? values.cors : null,
|
||||||
projectId: values.projectId,
|
projectId: values.projectId,
|
||||||
organizationId,
|
organizationId,
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const watch = useWatch({
|
|
||||||
control: form.control,
|
|
||||||
name: 'withSecret',
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ModalContent>
|
<ModalContent>
|
||||||
{mutation.isSuccess ? (
|
{mutation.isSuccess ? (
|
||||||
<>
|
<>
|
||||||
<ModalHeader
|
<ModalHeader title="Success" text={'Your client is created'} />
|
||||||
title="Success"
|
<CreateClientSuccess {...mutation.data} />
|
||||||
text={
|
<div className="flex gap-4 mt-4">
|
||||||
<>
|
<a
|
||||||
{mutation.data.clientSecret
|
className={cn(buttonVariants({ variant: 'secondary' }), 'flex-1')}
|
||||||
? 'Use your client id and secret with our SDK to send events to us. '
|
href="https://docs.openpanel.dev/docs"
|
||||||
: 'Use your client id with our SDK to send events to us. '}
|
target="_blank"
|
||||||
See our{' '}
|
|
||||||
<Link href="https//openpanel.dev/docs" className="underline">
|
|
||||||
documentation
|
|
||||||
</Link>
|
|
||||||
</>
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
<div className="grid gap-4">
|
|
||||||
<button
|
|
||||||
className="mt-4 text-left"
|
|
||||||
onClick={() => clipboard(mutation.data.clientId)}
|
|
||||||
>
|
|
||||||
<Label>Client ID</Label>
|
|
||||||
<div className="flex items-center justify-between rounded bg-gray-100 p-2 px-3">
|
|
||||||
{mutation.data.clientId}
|
|
||||||
<Copy size={16} />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
{mutation.data.clientSecret ? (
|
|
||||||
<button
|
|
||||||
className="mt-4 text-left"
|
|
||||||
onClick={() => clipboard(mutation.data.clientId)}
|
|
||||||
>
|
|
||||||
<Label>Secret</Label>
|
|
||||||
<div className="flex items-center justify-between rounded bg-gray-100 p-2 px-3">
|
|
||||||
{mutation.data.clientSecret}
|
|
||||||
<Copy size={16} />
|
|
||||||
</div>
|
|
||||||
</button>
|
|
||||||
) : (
|
|
||||||
<div className="mt-4 text-left">
|
|
||||||
<Label>Cors settings</Label>
|
|
||||||
<div className="flex items-center justify-between rounded bg-gray-100 p-2 px-3">
|
|
||||||
{mutation.data.cors}
|
|
||||||
</div>
|
|
||||||
<div className="text-sm italic mt-1">
|
|
||||||
You can update cors settings{' '}
|
|
||||||
<Link className="underline" href="/qwe/qwe/">
|
|
||||||
here
|
|
||||||
</Link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<DialogFooter>
|
|
||||||
<Button
|
|
||||||
type="button"
|
|
||||||
variant={'secondary'}
|
|
||||||
onClick={() => popModal()}
|
|
||||||
>
|
>
|
||||||
|
Read docs
|
||||||
|
</a>
|
||||||
|
<Button className="flex-1" onClick={() => popModal()}>
|
||||||
Close
|
Close
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</div>
|
||||||
</>
|
</>
|
||||||
) : (
|
) : (
|
||||||
<>
|
<>
|
||||||
@@ -144,37 +97,6 @@ export default function AddClient() {
|
|||||||
className="flex flex-col gap-4"
|
className="flex flex-col gap-4"
|
||||||
onSubmit={form.handleSubmit(onSubmit)}
|
onSubmit={form.handleSubmit(onSubmit)}
|
||||||
>
|
>
|
||||||
<div>
|
|
||||||
<Label>Client name</Label>
|
|
||||||
<Input
|
|
||||||
placeholder="Eg. My App Client"
|
|
||||||
error={form.formState.errors.name?.message}
|
|
||||||
{...form.register('name')}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<Controller
|
|
||||||
name="withSecret"
|
|
||||||
control={form.control}
|
|
||||||
render={({ field }) => (
|
|
||||||
<CheckboxInput
|
|
||||||
defaultChecked={!field.value}
|
|
||||||
onCheckedChange={(checked) => {
|
|
||||||
field.onChange(!checked);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
This is a website
|
|
||||||
</CheckboxInput>
|
|
||||||
)}
|
|
||||||
/>
|
|
||||||
<div>
|
|
||||||
<Label>Your domain name</Label>
|
|
||||||
<Input
|
|
||||||
placeholder="https://...."
|
|
||||||
error={form.formState.errors.domain?.message}
|
|
||||||
{...form.register('domain')}
|
|
||||||
disabled={watch}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
<div>
|
||||||
<Controller
|
<Controller
|
||||||
control={form.control}
|
control={form.control}
|
||||||
@@ -202,6 +124,37 @@ export default function AddClient() {
|
|||||||
}}
|
}}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label>Client name</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="Eg. My App Client"
|
||||||
|
error={form.formState.errors.name?.message}
|
||||||
|
{...form.register('name')}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<Tabs
|
||||||
|
defaultValue="website"
|
||||||
|
onValueChange={(val) => form.setValue('tab', val)}
|
||||||
|
className="h-28"
|
||||||
|
>
|
||||||
|
<TabsList className="bg-slate-200">
|
||||||
|
<TabsTrigger value="website">Website</TabsTrigger>
|
||||||
|
<TabsTrigger value="other">Other</TabsTrigger>
|
||||||
|
</TabsList>
|
||||||
|
<TabsContent value="website">
|
||||||
|
<Label>Cors</Label>
|
||||||
|
<Input
|
||||||
|
placeholder="https://example.com"
|
||||||
|
error={form.formState.errors.cors?.message}
|
||||||
|
{...form.register('cors')}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
|
<TabsContent value="other">
|
||||||
|
<div className="p-2 px-3 bg-white rounded text-sm">
|
||||||
|
🔑 You will get a secret to use for your API requests.
|
||||||
|
</div>
|
||||||
|
</TabsContent>
|
||||||
|
</Tabs>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
import { randomUUID } from 'crypto';
|
import { randomUUID } from 'crypto';
|
||||||
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
|
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
|
||||||
import { db } from '@/server/db';
|
import { db } from '@/server/db';
|
||||||
import { hashPassword } from '@openpanel/common';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { hashPassword } from '@openpanel/common';
|
||||||
|
|
||||||
export const clientRouter = createTRPCRouter({
|
export const clientRouter = createTRPCRouter({
|
||||||
list: protectedProcedure
|
list: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
@@ -59,7 +60,7 @@ export const clientRouter = createTRPCRouter({
|
|||||||
name: z.string(),
|
name: z.string(),
|
||||||
projectId: z.string(),
|
projectId: z.string(),
|
||||||
organizationId: z.string(),
|
organizationId: z.string(),
|
||||||
withCors: z.boolean().default(true),
|
cors: z.string().nullable(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.mutation(async ({ input }) => {
|
.mutation(async ({ input }) => {
|
||||||
@@ -69,41 +70,14 @@ export const clientRouter = createTRPCRouter({
|
|||||||
organization_slug: input.organizationId,
|
organization_slug: input.organizationId,
|
||||||
project_id: input.projectId,
|
project_id: input.projectId,
|
||||||
name: input.name,
|
name: input.name,
|
||||||
secret: input.withCors ? null : await hashPassword(secret),
|
secret: input.cors ? null : await hashPassword(secret),
|
||||||
|
cors: input.cors || undefined,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
clientSecret: input.withCors ? null : secret,
|
...client,
|
||||||
clientId: client.id,
|
secret: input.cors ? null : secret,
|
||||||
cors: client.cors,
|
|
||||||
};
|
|
||||||
}),
|
|
||||||
create2: protectedProcedure
|
|
||||||
.input(
|
|
||||||
z.object({
|
|
||||||
name: z.string(),
|
|
||||||
projectId: z.string(),
|
|
||||||
organizationId: z.string(),
|
|
||||||
domain: z.string().nullish(),
|
|
||||||
})
|
|
||||||
)
|
|
||||||
.mutation(async ({ input }) => {
|
|
||||||
const secret = randomUUID();
|
|
||||||
const client = await db.client.create({
|
|
||||||
data: {
|
|
||||||
organization_slug: input.organizationId,
|
|
||||||
project_id: input.projectId,
|
|
||||||
name: input.name,
|
|
||||||
secret: input.domain ? undefined : await hashPassword(secret),
|
|
||||||
cors: input.domain || undefined,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
return {
|
|
||||||
clientSecret: input.domain ? null : secret,
|
|
||||||
clientId: client.id,
|
|
||||||
cors: client.cors,
|
|
||||||
};
|
};
|
||||||
}),
|
}),
|
||||||
remove: protectedProcedure
|
remove: protectedProcedure
|
||||||
|
|||||||
@@ -1,14 +1,18 @@
|
|||||||
|
import { randomUUID } from 'crypto';
|
||||||
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
|
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
|
||||||
import { clerkClient } from '@clerk/nextjs';
|
import { clerkClient } from '@clerk/nextjs';
|
||||||
import { db } from '@openpanel/db';
|
|
||||||
import { z } from 'zod';
|
import { z } from 'zod';
|
||||||
|
|
||||||
|
import { hashPassword } from '@openpanel/common';
|
||||||
|
import { db } from '@openpanel/db';
|
||||||
|
|
||||||
export const onboardingRouter = createTRPCRouter({
|
export const onboardingRouter = createTRPCRouter({
|
||||||
organziation: protectedProcedure
|
organziation: protectedProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
organization: z.string(),
|
organization: z.string(),
|
||||||
project: z.string().optional(),
|
project: z.string(),
|
||||||
|
cors: z.string().nullable(),
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.mutation(async ({ input, ctx }) => {
|
.mutation(async ({ input, ctx }) => {
|
||||||
@@ -17,7 +21,7 @@ export const onboardingRouter = createTRPCRouter({
|
|||||||
createdBy: ctx.session.userId,
|
createdBy: ctx.session.userId,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (org.slug && input.project) {
|
if (org.slug) {
|
||||||
const project = await db.project.create({
|
const project = await db.project.create({
|
||||||
data: {
|
data: {
|
||||||
name: input.project,
|
name: input.project,
|
||||||
@@ -25,13 +29,29 @@ export const onboardingRouter = createTRPCRouter({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const secret = randomUUID();
|
||||||
|
const client = await db.client.create({
|
||||||
|
data: {
|
||||||
|
name: `${project.name} Client`,
|
||||||
|
organization_slug: org.slug,
|
||||||
|
project_id: project.id,
|
||||||
|
cors: input.cors ?? '*',
|
||||||
|
secret: input.cors ? null : await hashPassword(secret),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
client: {
|
||||||
|
...client,
|
||||||
|
secret,
|
||||||
|
},
|
||||||
project,
|
project,
|
||||||
organization: org,
|
organization: org,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
client: null,
|
||||||
project: null,
|
project: null,
|
||||||
organization: org,
|
organization: org,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,7 +1,12 @@
|
|||||||
import { createTRPCRouter, protectedProcedure } from '@/server/api/trpc';
|
import {
|
||||||
|
createTRPCRouter,
|
||||||
|
protectedProcedure,
|
||||||
|
publicProcedure,
|
||||||
|
} from '@/server/api/trpc';
|
||||||
|
import { z } from 'zod';
|
||||||
|
|
||||||
import { db, getReferences } from '@openpanel/db';
|
import { db, getReferences } from '@openpanel/db';
|
||||||
import { zCreateReference, zRange } from '@openpanel/validation';
|
import { zCreateReference, zRange } from '@openpanel/validation';
|
||||||
import { z } from 'zod';
|
|
||||||
|
|
||||||
import { getChartStartEndDate } from './chart.helpers';
|
import { getChartStartEndDate } from './chart.helpers';
|
||||||
|
|
||||||
@@ -29,7 +34,7 @@ export const referenceRouter = createTRPCRouter({
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
getChartReferences: protectedProcedure
|
getChartReferences: publicProcedure
|
||||||
.input(
|
.input(
|
||||||
z.object({
|
z.object({
|
||||||
projectId: z.string(),
|
projectId: z.string(),
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ export default RootLayout({ children }) {
|
|||||||
trackScreenViews={true}
|
trackScreenViews={true}
|
||||||
// trackAttributes={true}
|
// trackAttributes={true}
|
||||||
// trackOutgoingLinks={true}
|
// trackOutgoingLinks={true}
|
||||||
|
// If you have a user id, you can pass it here to identify the user
|
||||||
|
// profileId={'123'}
|
||||||
/>
|
/>
|
||||||
{children}
|
{children}
|
||||||
</>
|
</>
|
||||||
|
|||||||
31
pnpm-lock.yaml
generated
31
pnpm-lock.yaml
generated
@@ -171,6 +171,9 @@ importers:
|
|||||||
'@radix-ui/react-slot':
|
'@radix-ui/react-slot':
|
||||||
specifier: ^1.0.2
|
specifier: ^1.0.2
|
||||||
version: 1.0.2(@types/react@18.2.56)(react@18.2.0)
|
version: 1.0.2(@types/react@18.2.56)(react@18.2.0)
|
||||||
|
'@radix-ui/react-tabs':
|
||||||
|
specifier: ^1.0.4
|
||||||
|
version: 1.0.4(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0)
|
||||||
'@radix-ui/react-toast':
|
'@radix-ui/react-toast':
|
||||||
specifier: ^1.1.5
|
specifier: ^1.1.5
|
||||||
version: 1.1.5(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0)
|
version: 1.1.5(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0)
|
||||||
@@ -5462,6 +5465,34 @@ packages:
|
|||||||
react: 18.2.0
|
react: 18.2.0
|
||||||
dev: false
|
dev: false
|
||||||
|
|
||||||
|
/@radix-ui/react-tabs@1.0.4(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0):
|
||||||
|
resolution: {integrity: sha512-egZfYY/+wRNCflXNHx+dePvnz9FbmssDTJBtgRfDY7e8SE5oIo3Py2eCB1ckAbh1Q7cQ/6yJZThJ++sgbxibog==}
|
||||||
|
peerDependencies:
|
||||||
|
'@types/react': '*'
|
||||||
|
'@types/react-dom': '*'
|
||||||
|
react: ^16.8 || ^17.0 || ^18.0
|
||||||
|
react-dom: ^16.8 || ^17.0 || ^18.0
|
||||||
|
peerDependenciesMeta:
|
||||||
|
'@types/react':
|
||||||
|
optional: true
|
||||||
|
'@types/react-dom':
|
||||||
|
optional: true
|
||||||
|
dependencies:
|
||||||
|
'@babel/runtime': 7.23.9
|
||||||
|
'@radix-ui/primitive': 1.0.1
|
||||||
|
'@radix-ui/react-context': 1.0.1(@types/react@18.2.56)(react@18.2.0)
|
||||||
|
'@radix-ui/react-direction': 1.0.1(@types/react@18.2.56)(react@18.2.0)
|
||||||
|
'@radix-ui/react-id': 1.0.1(@types/react@18.2.56)(react@18.2.0)
|
||||||
|
'@radix-ui/react-presence': 1.0.1(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-primitive': 1.0.3(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-roving-focus': 1.0.4(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0)
|
||||||
|
'@radix-ui/react-use-controllable-state': 1.0.1(@types/react@18.2.56)(react@18.2.0)
|
||||||
|
'@types/react': 18.2.56
|
||||||
|
'@types/react-dom': 18.2.19
|
||||||
|
react: 18.2.0
|
||||||
|
react-dom: 18.2.0(react@18.2.0)
|
||||||
|
dev: false
|
||||||
|
|
||||||
/@radix-ui/react-toast@1.1.5(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0):
|
/@radix-ui/react-toast@1.1.5(@types/react-dom@18.2.19)(@types/react@18.2.56)(react-dom@18.2.0)(react@18.2.0):
|
||||||
resolution: {integrity: sha512-fRLn227WHIBRSzuRzGJ8W+5YALxofH23y0MlPLddaIpLpCDqdE0NZlS2NRQDRiptfxDeeCjgFIpexB1/zkxDlw==}
|
resolution: {integrity: sha512-fRLn227WHIBRSzuRzGJ8W+5YALxofH23y0MlPLddaIpLpCDqdE0NZlS2NRQDRiptfxDeeCjgFIpexB1/zkxDlw==}
|
||||||
peerDependencies:
|
peerDependencies:
|
||||||
|
|||||||
Reference in New Issue
Block a user