Skip to content

Commit

Permalink
style(dashboard): improve keys page design and look and feel (#7236)
Browse files Browse the repository at this point in the history
  • Loading branch information
scopsy authored Dec 8, 2024
1 parent 7dc12c2 commit 64a1c99
Show file tree
Hide file tree
Showing 8 changed files with 204 additions and 71 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ export const HeaderNavigation = (props: HeaderNavigationProps) => {
const { startItems, hideBridgeUrl = false, className, ...rest } = props;
return (
<div
className={cn('bg-background flex h-12 w-full items-center justify-between border-b px-2.5 py-1.5', className)}
className={cn(
'bg-background flex h-12 w-full items-center justify-between border-b border-b-neutral-100 px-2.5 py-1.5',
className
)}
{...rest}
>
{startItems}
Expand Down
6 changes: 5 additions & 1 deletion apps/dashboard/src/components/primitives/card.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,11 @@ Card.displayName = 'Card';

const CardHeader = React.forwardRef<HTMLDivElement, React.HTMLAttributes<HTMLDivElement>>(
({ className, ...props }, ref) => (
<div ref={ref} className={cn('flex flex-col space-y-1.5 p-6', className)} {...props} />
<div
ref={ref}
className={cn('flex flex-col space-y-1.5 bg-neutral-50 p-4 text-sm font-medium', className)}
{...props}
/>
)
);
CardHeader.displayName = 'CardHeader';
Expand Down
5 changes: 5 additions & 0 deletions apps/dashboard/src/components/primitives/container.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import { cn } from '../../utils/ui';

export const Container = ({ children, className }: { children: React.ReactNode; className?: string }) => {
return <div className={cn('mx-auto w-full max-w-[1152px] px-14 py-14', className)}>{children}</div>;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { RiInformation2Line } from 'react-icons/ri';
import { Tooltip, TooltipContent, TooltipTrigger } from './tooltip';
import { cn } from '../../utils/ui';

interface HelpTooltipIndicatorProps {
text: string;
className?: string;
size?: '4' | '5';
}

export function HelpTooltipIndicator({ text, className, size = '5' }: HelpTooltipIndicatorProps) {
return (
<Tooltip>
<TooltipTrigger asChild>
<span className={cn('text-foreground-400 hover:cursor inline-block', className)}>
<RiInformation2Line className={`size-${size}`} />
</span>
</TooltipTrigger>
<TooltipContent>
<p>{text}</p>
</TooltipContent>
</Tooltip>
);
}
31 changes: 27 additions & 4 deletions apps/dashboard/src/components/shared/external-link.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,44 @@
import { RiExternalLinkLine } from 'react-icons/ri';
import { RiBookMarkedLine, RiExternalLinkLine, RiQuestionLine } from 'react-icons/ri';
import { cn } from '@/utils/ui';
import { useTelemetry } from '@/hooks/use-telemetry';
import { TelemetryEvent } from '@/utils/telemetry';

interface ExternalLinkProps extends React.AnchorHTMLAttributes<HTMLAnchorElement> {
children: React.ReactNode;
iconClassName?: string;
variant?: 'default' | 'documentation' | 'tip';
}

export function ExternalLink({ children, className, iconClassName, ...props }: ExternalLinkProps) {
export function ExternalLink({
children,
className,
variant = 'default',
iconClassName,
href,
...props
}: ExternalLinkProps) {
const telemetry = useTelemetry();

const handleClick = () => {
telemetry(TelemetryEvent.EXTERNAL_LINK_CLICKED, {
href,
variant,
});
};

return (
<a
target="_blank"
rel="noopener noreferrer"
className={cn('inline-flex items-center gap-1 hover:underline', className)}
className={cn('text-foreground-600 inline-flex items-center gap-1 hover: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}
<RiExternalLinkLine className={cn('size-4', iconClassName)} aria-hidden="true" />
</a>
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -70,7 +70,7 @@ export const SideNavigation = () => {
<RiStore3Line className="size-4" />
<span>Integration Store</span>
</NavigationLink>
<NavigationLink to={ROUTES.API_KEYS}>
<NavigationLink to={buildRoute(ROUTES.API_KEYS, { environmentSlug: currentEnvironment?.slug ?? '' })}>
<RiKey2Line className="size-4" />
<span>API Keys</span>
</NavigationLink>
Expand Down
201 changes: 137 additions & 64 deletions apps/dashboard/src/pages/api-keys.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import { useState } from 'react';
import { RiKey2Line, RiEyeLine, RiEyeOffLine } from 'react-icons/ri';
import { useEnvironment } from '@/context/environment/hooks';
import { CopyButton } from '@/components/primitives/copy-button';
import { Card, CardContent } from '@/components/primitives/card';
import { Card, CardContent, CardHeader } from '@/components/primitives/card';
import { Button } from '@/components/primitives/button';
import { Input, InputField } from '@/components/primitives/input';
import { Form } from '@/components/primitives/form/form';
Expand All @@ -11,6 +11,10 @@ import { DashboardLayout } from '../components/dashboard-layout';
import { PageMeta } from '@/components/page-meta';
import { useFetchApiKeys } from '../hooks/use-fetch-api-keys';
import { ExternalLink } from '@/components/shared/external-link';
import { Container } from '../components/primitives/container';
import { HelpTooltipIndicator } from '../components/primitives/help-tooltip-indicator';
import { API_HOSTNAME } from '../config';
import { Skeleton } from '@/components/primitives/skeleton';

interface ApiKeysFormData {
apiKey: string;
Expand All @@ -21,8 +25,8 @@ interface ApiKeysFormData {
export function ApiKeysPage() {
const apiKeysQuery = useFetchApiKeys();
const { currentEnvironment } = useEnvironment();
const [showApiKey, setShowApiKey] = useState(false);
const apiKeys = apiKeysQuery.data?.data;
const isLoading = apiKeysQuery.isLoading;

const form = useForm<ApiKeysFormData>({
values: {
Expand All @@ -36,80 +40,149 @@ export function ApiKeysPage() {
return null;
}

const toggleApiKeyVisibility = () => {
setShowApiKey(!showApiKey);
};

const maskApiKey = (key: string) => {
return `${'•'.repeat(28)} ${key.slice(-4)}`;
};

return (
<>
<PageMeta title={`API Keys for ${currentEnvironment?.name} environment`} />
<DashboardLayout headerStartItems={<h1 className="text-foreground-950">API Keys</h1>}>
<div className="flex flex-col gap-6 p-6">
<div className="grid grid-cols-1 gap-6 lg:grid-cols-[1fr,500px]">
<Form {...form}>
<Card className="shadow-none">
<CardContent className="p-6">
<div className="space-y-4">
<div className="space-y-2">
<label className="text-foreground-600 text-sm font-medium">Secret Key</label>
<div className="flex items-center gap-2">
<InputField className="flex overflow-hidden pr-0">
<Input
className="cursor-default"
value={showApiKey ? form.getValues('apiKey') : maskApiKey(form.getValues('apiKey'))}
readOnly
/>
<CopyButton size="input-right" valueToCopy={form.getValues('apiKey')} />
</InputField>

<Button
variant="outline"
size="icon"
onClick={toggleApiKeyVisibility}
aria-label={showApiKey ? 'Hide API Key' : 'Show API Key'}
>
{showApiKey ? <RiEyeOffLine className="size-4" /> : <RiEyeLine className="size-4" />}
</Button>
</div>
<p className="text-foreground-600 text-xs">
Use this key to authenticate your API requests. Keep it secure and never share it publicly.
</p>
</div>

<div className="space-y-2">
<label className="text-foreground-600 text-sm font-medium">Application Identifier</label>
<div className="flex items-center gap-2">
<InputField className="flex overflow-hidden pr-0">
<Input className="cursor-default" value={form.getValues('identifier')} readOnly />
<CopyButton size="input-right" valueToCopy={form.getValues('identifier')} />
</InputField>
</div>
<p className="text-foreground-600 text-xs">
The public application identifier used for the Inbox component
</p>
</div>
</div>
</CardContent>
</Card>
</Form>
<div className="column flex gap-2 p-6 pt-0">
<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-10 w-10" />
<h2 className="text-foreground-950 text-lg font-medium">Environment Keys</h2>
<p className="text-foreground-400 text-md">Copy and manage your public and private keys</p>
<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 href="https://docs.novu.co/sdks/overview" className="text-sm">
<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
</ExternalLink>
</div>
</Form>
</div>
</div>
</div>
</Container>
</DashboardLayout>
</>
);
}

interface SettingFieldProps {
label: string;
tooltip?: string;
value?: string;
secret?: boolean;
isLoading?: boolean;
readOnly?: boolean;
}

function SettingField({
label,
tooltip,
value,
secret = false,
isLoading = false,
readOnly = true,
}: SettingFieldProps) {
const [showSecret, setShowSecret] = useState(false);

const toggleSecretVisibility = () => {
setShowSecret(!showSecret);
};

const maskSecret = (secret: string) => {
return `${'•'.repeat(28)} ${secret.slice(-4)}`;
};

return (
<div className="grid grid-cols-[1fr,400px] items-start gap-3">
<label className={`text-foreground-950 text-xs font-medium`}>
{label}
{tooltip && <HelpTooltipIndicator text={tooltip} className="relative top-[5px] ml-1" />}
</label>
<div className="flex items-center gap-2">
{isLoading ? (
<>
<Skeleton className="h-[38px] flex-1 rounded-lg" />
{secret && <Skeleton className="h-[38px] w-[38px] rounded-lg" />}
</>
) : (
<>
<InputField className="flex overflow-hidden pr-0">
<Input
className="cursor-default"
value={secret ? (showSecret ? value : maskSecret(value ?? '')) : value}
readOnly={readOnly}
/>
<CopyButton size="input-right" valueToCopy={value ?? ''} />
</InputField>

{secret && (
<Button
variant="outline"
size="icon"
onClick={toggleSecretVisibility}
disabled={isLoading}
aria-label={showSecret ? 'Hide Secret' : 'Show Secret'}
>
{showSecret ? <RiEyeOffLine className="size-4" /> : <RiEyeLine className="size-4" />}
</Button>
)}
</>
)}
</div>
</div>
);
}
1 change: 1 addition & 0 deletions apps/dashboard/src/utils/telemetry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,4 +22,5 @@ export enum TelemetryEvent {
INBOX_EMBED_SUCCESS_PAGE_VIEWED = 'Inbox embed success page viewed - [Onboarding]',
WORKFLOW_PREFERENCES_OVERRIDE_USED = 'Workflow preferences override used',
EXPORT_TO_CODE_BANNER_REACTION = 'Export to Code banner reaction - [Promotional]',
EXTERNAL_LINK_CLICKED = 'External link clicked',
}

0 comments on commit 64a1c99

Please sign in to comment.