diff --git a/apps/dashboard/src/components/create-workflow-button.tsx b/apps/dashboard/src/components/create-workflow-button.tsx index 1adebbae0d4..d999cb1f37d 100644 --- a/apps/dashboard/src/components/create-workflow-button.tsx +++ b/apps/dashboard/src/components/create-workflow-button.tsx @@ -2,8 +2,9 @@ import { zodResolver } from '@hookform/resolvers/zod'; import { useMutation, useQueryClient } from '@tanstack/react-query'; import { ComponentProps, useState } from 'react'; import { useForm } from 'react-hook-form'; -import { RiExternalLinkLine } from 'react-icons/ri'; -import { Link, useNavigate } from 'react-router-dom'; +import { RiArrowRightSLine } from 'react-icons/ri'; +import { ExternalLink } from '@/components/shared/external-link'; +import { useNavigate } from 'react-router-dom'; import { z } from 'zod'; import { type CreateWorkflowDto, WorkflowCreationSourceEnum, slugify } from '@novu/shared'; import { createWorkflow } from '@/api/workflows'; @@ -73,13 +74,7 @@ export const CreateWorkflowButton = (props: CreateWorkflowButtonProps) => { <div> <SheetDescription> Define the steps to notify subscribers using channels like in-app, email, and more.{' '} - <Link - target="_blank" - to="https://docs.novu.co/concepts/workflows" - className="text-foreground-400 inline-flex items-center text-xs underline" - > - Learn more <RiExternalLinkLine className="inline size-4" /> - </Link> + <ExternalLink href="https://docs.novu.co/concepts/workflows">Learn more</ExternalLink> </SheetDescription> </div> </SheetHeader> @@ -195,6 +190,7 @@ export const CreateWorkflowButton = (props: CreateWorkflowButtonProps) => { <SheetFooter> <Button isLoading={isPending} variant="default" type="submit" form="create-workflow"> Create workflow + <RiArrowRightSLine className="size-4" /> </Button> </SheetFooter> </SheetContent> diff --git a/apps/dashboard/src/components/header-navigation/edit-bridge-url-button.tsx b/apps/dashboard/src/components/header-navigation/edit-bridge-url-button.tsx index 6d120015218..85183462719 100644 --- a/apps/dashboard/src/components/header-navigation/edit-bridge-url-button.tsx +++ b/apps/dashboard/src/components/header-navigation/edit-bridge-url-button.tsx @@ -63,7 +63,7 @@ export const EditBridgeUrlButton = () => { className={cn( 'relative size-1.5 animate-[pulse-shadow_1s_ease-in-out_infinite] rounded-full', status === ConnectionStatus.DISCONNECTED || status === ConnectionStatus.LOADING - ? 'bg-destructive [--pulse-color:var(--destructive)]' + ? 'bg-destructive' : 'bg-success [--pulse-color:var(--success)]' )} /> diff --git a/apps/dashboard/src/components/primitives/button.tsx b/apps/dashboard/src/components/primitives/button.tsx index 617b4758723..993a4322530 100644 --- a/apps/dashboard/src/components/primitives/button.tsx +++ b/apps/dashboard/src/components/primitives/button.tsx @@ -5,7 +5,7 @@ import { cn } from '@/utils/ui'; import { RiLoader4Line } from 'react-icons/ri'; export const buttonVariants = cva( - `relative isolate inline-flex items-center justify-center whitespace-nowrap rounded-lg gap-1 text-sm font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50`, + `relative isolate inline-flex items-center justify-center whitespace-nowrap rounded-lg gap-1 text-xs font-medium transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring disabled:pointer-events-none disabled:opacity-50`, { variants: { variant: { @@ -26,10 +26,10 @@ export const buttonVariants = cva( }, size: { default: 'h-9 p-2.5', - xs: 'h-6 px-1.5 rounded-md text-xs', - sm: 'h-8 px-1.5 rounded-md text-xs', + xs: 'h-6 px-2 rounded-md', + sm: 'h-8 px-2 rounded-md', lg: 'h-10 rounded-md px-8', - 'input-right': 'rounded-none border-b-0 h-full text-xs border-r-0 border-t-0 px-2 py-0', + 'input-right': 'rounded-none border-b-0 h-full border-r-0 border-t-0 px-2 py-0', icon: 'size-8', }, }, diff --git a/apps/dashboard/src/components/shared/external-link.tsx b/apps/dashboard/src/components/shared/external-link.tsx index 483c083a4af..bf1cba85309 100644 --- a/apps/dashboard/src/components/shared/external-link.tsx +++ b/apps/dashboard/src/components/shared/external-link.tsx @@ -1,4 +1,4 @@ -import { RiBookMarkedLine, RiExternalLinkLine, RiQuestionLine } from 'react-icons/ri'; +import { RiBookMarkedLine, RiArrowRightUpLine, RiQuestionLine } from 'react-icons/ri'; import { cn } from '@/utils/ui'; import { useTelemetry } from '@/hooks/use-telemetry'; import { TelemetryEvent } from '@/utils/telemetry'; @@ -26,19 +26,21 @@ export function ExternalLink({ }); }; + const finalIconClassName = cn('inline size-3 mb-1', iconClassName); + return ( <a target="_blank" rel="noopener noreferrer" - className={cn('text-foreground-600 inline-flex items-center gap-1 hover:underline', className)} + className={cn('text-foreground-400 inline-flex items-center text-xs underline', className)} href={href} onClick={handleClick} {...props} > - {variant === 'documentation' && <RiBookMarkedLine className={cn('size-4', iconClassName)} aria-hidden="true" />} - {variant === 'default' && <RiExternalLinkLine className={cn('size-4', iconClassName)} aria-hidden="true" />} - {variant === 'tip' && <RiQuestionLine className={cn('size-4', iconClassName)} aria-hidden="true" />} {children} + {variant === 'documentation' && <RiBookMarkedLine className={finalIconClassName} aria-hidden="true" />} + {variant === 'default' && <RiArrowRightUpLine className={finalIconClassName} aria-hidden="true" />} + {variant === 'tip' && <RiQuestionLine className={finalIconClassName} aria-hidden="true" />} </a> ); } diff --git a/apps/dashboard/src/components/side-navigation/free-trial-card.tsx b/apps/dashboard/src/components/side-navigation/free-trial-card.tsx index a490217f878..e60c997b204 100644 --- a/apps/dashboard/src/components/side-navigation/free-trial-card.tsx +++ b/apps/dashboard/src/components/side-navigation/free-trial-card.tsx @@ -53,7 +53,7 @@ const CardContent = ({ </Tooltip> </div> <span className="text-foreground-600 text-xs"> - Experience Novu without any limits for free for the next {pluralizedDays}. + Enjoy unlimited access to Novu for free for the next {pluralizedDays}. </span> <div className={`max-h-3 overflow-hidden opacity-100 ${transition} group-hover:max-h-0 group-hover:opacity-0`}> <Progress value={daysTotal - daysLeft} max={daysTotal} /> diff --git a/apps/dashboard/src/components/user-profile.tsx b/apps/dashboard/src/components/user-profile.tsx index a7dd7b3f99b..f1702bd1430 100644 --- a/apps/dashboard/src/components/user-profile.tsx +++ b/apps/dashboard/src/components/user-profile.tsx @@ -1,9 +1,10 @@ -import { UserButton } from '@clerk/clerk-react'; +import { UserButton, useOrganization } from '@clerk/clerk-react'; import { useNewDashboardOptIn } from '@/hooks/use-new-dashboard-opt-in'; import { RiSignpostFill } from 'react-icons/ri'; import { ROUTES } from '../utils/routes'; export function UserProfile() { + const { organization } = useOrganization(); const { optOut } = useNewDashboardOptIn(); return ( @@ -16,13 +17,15 @@ export function UserProfile() { }, }} > - <UserButton.MenuItems> - <UserButton.Action - label="Go back to the legacy Dashboard" - labelIcon={<RiSignpostFill size="16" color="var(--nv-colors-typography-text-main)" />} - onClick={optOut} - /> - </UserButton.MenuItems> + {organization && organization.createdAt < new Date('2024-12-24') && ( + <UserButton.MenuItems> + <UserButton.Action + label="Go back to the legacy Dashboard" + labelIcon={<RiSignpostFill size="16" color="var(--nv-colors-typography-text-main)" />} + onClick={optOut} + /> + </UserButton.MenuItems> + )} </UserButton> ); } diff --git a/apps/dashboard/src/components/workflow-list-empty.tsx b/apps/dashboard/src/components/workflow-list-empty.tsx index df277a9ca32..ff7bf93913d 100644 --- a/apps/dashboard/src/components/workflow-list-empty.tsx +++ b/apps/dashboard/src/components/workflow-list-empty.tsx @@ -50,12 +50,10 @@ const WorkflowListEmptyDev = () => ( <div className="flex h-full w-full flex-col items-center justify-center gap-6"> <VersionControlDev /> <div className="flex flex-col items-center gap-2 text-center"> - <span className="text-foreground-900 block font-medium"> - Create your first workflow to orchestrate notifications - </span> + <span className="text-foreground-900 block font-medium">Create your first workflow to send notifications</span> <p className="text-foreground-400 max-w-[60ch] text-sm"> - Workflows in Novu handle event-driven notifications across multiple channels in a single, version-controlled - flow, with the ability to manage preference for each subscriber. + Workflows handle notifications across multiple channels in a single, version-controlled flow, with the ability + to manage preference for each subscriber. </p> </div> diff --git a/apps/dashboard/src/context/clerk-provider.tsx b/apps/dashboard/src/context/clerk-provider.tsx index 259aa679531..064ea1f48f1 100644 --- a/apps/dashboard/src/context/clerk-provider.tsx +++ b/apps/dashboard/src/context/clerk-provider.tsx @@ -6,31 +6,6 @@ import { PropsWithChildren } from 'react'; import { useNavigate } from 'react-router-dom'; import { ROUTES } from '../utils/routes'; -const CLERK_LOCALIZATION = { - userProfile: { - navbar: { - title: 'Settings', - description: '', - account: 'User profile', - security: 'Access security', - }, - }, - organizationProfile: { - membersPage: { - requestsTab: { autoSuggestions: { headerTitle: '' } }, - invitationsTab: { autoInvitations: { headerTitle: '' } }, - }, - }, - userButton: { - action__signOut: 'Log out', - action__signOutAll: 'Log out from all accounts', - action__manageAccount: 'Settings', - }, - formFieldLabel__organizationSlug: 'URL friendly identifier', -}; - -const ALLOWED_REDIRECT_ORIGINS = ['http://localhost:*', window.location.origin]; - type ClerkProviderProps = PropsWithChildren; export const ClerkProvider = (props: ClerkProviderProps) => { const navigate = useNavigate(); @@ -61,6 +36,16 @@ export const ClerkProvider = (props: ClerkProviderProps) => { }, }, }, + organizationList: { + elements: { + cardBox: { + borderRadius: '0', + }, + card: { + borderRadius: '0', + }, + }, + }, elements: { formButtonPrimary: cn(buttonVariants({ variant: 'primary' })), }, @@ -68,8 +53,32 @@ export const ClerkProvider = (props: ClerkProviderProps) => { fontSize: '14px !important', }, }} - localization={CLERK_LOCALIZATION} - allowedRedirectOrigins={ALLOWED_REDIRECT_ORIGINS} + localization={{ + userProfile: { + navbar: { + title: 'Settings', + description: '', + account: 'User profile', + security: 'Access security', + }, + }, + organizationProfile: { + membersPage: { + requestsTab: { autoSuggestions: { headerTitle: '' } }, + invitationsTab: { autoInvitations: { headerTitle: '' } }, + }, + }, + userButton: { + action__signOut: 'Log out', + action__signOutAll: 'Log out from all accounts', + action__manageAccount: 'Settings', + }, + formFieldLabel__organizationSlug: 'URL friendly identifier', + unstable__errors: { + form_identifier_exists: 'Already taken, please choose another', + }, + }} + allowedRedirectOrigins={['http://localhost:*', window.location.origin]} > {children} </_ClerkProvider> diff --git a/apps/dashboard/src/pages/api-keys.tsx b/apps/dashboard/src/pages/api-keys.tsx index 033d68ee3a4..e1058b7402a 100644 --- a/apps/dashboard/src/pages/api-keys.tsx +++ b/apps/dashboard/src/pages/api-keys.tsx @@ -1,5 +1,5 @@ import { useState } from 'react'; -import { RiKey2Line, RiEyeLine, RiEyeOffLine } from 'react-icons/ri'; +import { RiEyeLine, RiEyeOffLine } from 'react-icons/ri'; import { useEnvironment } from '@/context/environment/hooks'; import { CopyButton } from '@/components/primitives/copy-button'; import { Card, CardContent, CardHeader } from '@/components/primitives/card'; @@ -40,94 +40,85 @@ export function ApiKeysPage() { return null; } + const region = window.location.hostname.includes('eu') ? 'EU' : 'US'; + return ( <> <PageMeta title={`API Keys for ${currentEnvironment?.name} environment`} /> <DashboardLayout headerStartItems={<h1 className="text-foreground-950">API Keys</h1>}> - <Container> - <div className="grid grid-cols-1 gap-6 lg:grid-cols-[224px,1fr]"> - <div className="column flex gap-2 pt-0"> - <div className="flex flex-col gap-2"> - <RiKey2Line className="h-8 w-8" /> - <h2 className="text-foreground-950 text-md font-medium">Environment Keys</h2> - <p className="text-foreground-400 text-xs">Manage your public and private keys</p> - - <ExternalLink variant="documentation" href="https://docs.novu.co/sdks/overview" className="text-sm"> - Read about our SDKs - </ExternalLink> - </div> - </div> - <div className="ml-auto flex w-full max-w-[700px] flex-col gap-6"> - <Form {...form}> - <Card className="w-full overflow-hidden shadow-none"> - <CardHeader>Application</CardHeader> - - <CardContent className="rounded-b-xl border-t bg-neutral-50 bg-white p-3"> - <div className="space-y-4 p-3"> - <SettingField - label="API URL" - tooltip="The base URL for making API requests to Novu" - value={API_HOSTNAME} - /> - - <SettingField - label="Application Identifier" - tooltip="This is a unique identifier for the current environment, used to initialize the Inbox component" - value={form.getValues('identifier')} - isLoading={isLoading} - /> - </div> - </CardContent> - </Card> - - <div> - <Card className="w-full overflow-hidden shadow-none"> - <CardHeader> - Secret Keys - <p className="text-foreground-600 mt-1 text-xs"> - Use this key to authenticate your API requests. Keep it secure and never share it publicly. - </p> - </CardHeader> - - <CardContent className="rounded-b-xl border-t bg-neutral-50 bg-white p-3"> - <div className="space-y-4 p-3"> - <SettingField - label="Secret Key" - tooltip="Use this key to authenticate your API requests. Keep it secure and never share it publicly." - value={form.getValues('apiKey')} - secret - isLoading={isLoading} - /> - </div> - </CardContent> - </Card> - <ExternalLink - variant="tip" - iconClassName="text-neutral-400" - href="https://docs.novu.co/api-reference/overview" - className="mt-2 text-xs text-neutral-600" - > - Learn more about our APIs + <Container className="flex w-full max-w-[800px] flex-col gap-6"> + <Form {...form}> + <Card className="w-full overflow-hidden shadow-none"> + <CardHeader> + {'<Inbox />'} + <p className="text-foreground-500 mt-1 text-xs font-normal"> + {'Use the public application identifier in Novu <Inbox />. '} + <ExternalLink href="https://docs.novu.co/inbox/overview" className="text-foreground-500"> + Learn more </ExternalLink> + </p> + </CardHeader> + <CardContent className="rounded-b-xl border-t bg-neutral-50 bg-white p-3"> + <div className="space-y-4 p-3"> + <SettingField + label="Application Identifier" + tooltip={`This is unique for the ${currentEnvironment.name} environment.`} + value={form.getValues('identifier')} + isLoading={isLoading} + /> </div> - </Form> - </div> - </div> + </CardContent> + </Card> + <Card className="w-full overflow-hidden shadow-none"> + <CardHeader> + Secret Keys + <p className="text-foreground-500 mt-1 text-xs font-normal"> + {'Use the secret key to authenticate your SDK requests. Keep it secure and never share it publicly. '} + <ExternalLink href="https://docs.novu.co/sdks/overview" className="text-foreground-500"> + Learn more + </ExternalLink> + </p> + </CardHeader> + + <CardContent className="rounded-b-xl border-t bg-neutral-50 bg-white p-3"> + <div className="space-y-4 p-3"> + <SettingField + label="Secret Key" + tooltip="Keep it secure and never share it publicly" + value={form.getValues('apiKey')} + secret + isLoading={isLoading} + /> + </div> + </CardContent> + </Card> + <Card className="w-full overflow-hidden shadow-none"> + <CardHeader> + API URLs + <p className="text-foreground-500 mt-1 text-xs font-normal"> + {`URLs for Novu Cloud in the ${region} region. `} + <ExternalLink href="https://docs.novu.co/api-reference/overview" className="text-foreground-500"> + Learn more + </ExternalLink> + </p> + </CardHeader> + <CardContent className="rounded-b-xl border-t bg-neutral-50 bg-white p-3"> + <div className="space-y-4 p-3"> + <SettingField + label="Novu API Hostname" + tooltip={`For Novu Cloud in the ${region} region`} + value={API_HOSTNAME} + /> + </div> + </CardContent> + </Card> + </Form> </Container> </DashboardLayout> </> ); } -interface SettingFieldProps { - label: string; - tooltip?: string; - value?: string; - secret?: boolean; - isLoading?: boolean; - readOnly?: boolean; -} - function SettingField({ label, tooltip, @@ -173,6 +164,8 @@ function SettingField({ <Button variant="outline" size="icon" + // TODO: Icon size variant is size-8 but doesn't align with the size of the input. We should fix this. + className="size-9" onClick={toggleSecretVisibility} disabled={isLoading} aria-label={showSecret ? 'Hide Secret' : 'Show Secret'} diff --git a/apps/dashboard/src/pages/workflows.tsx b/apps/dashboard/src/pages/workflows.tsx index 5c889f23466..9be66f68930 100644 --- a/apps/dashboard/src/pages/workflows.tsx +++ b/apps/dashboard/src/pages/workflows.tsx @@ -1,5 +1,5 @@ import { useEffect } from 'react'; - +import { RiRouteFill } from 'react-icons/ri'; import { WorkflowList } from '@/components/workflow-list'; import { DashboardLayout } from '@/components/dashboard-layout'; import { Button } from '@/components/primitives/button'; @@ -36,6 +36,7 @@ export const WorkflowsPage = () => { <CreateWorkflowButton asChild> <Button variant="primary" size="sm"> + <RiRouteFill className="size-4" /> Create workflow </Button> </CreateWorkflowButton>