Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

style(dashboard): improve keys page design and look and feel #7236

Merged
merged 10 commits into from
Dec 8, 2024
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
)}
Comment on lines +17 to +19
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed the color to match Figma designs of the bottom border

{...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}
/>
Comment on lines +12 to +16
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Updated card bg to reflect design system

)
);
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>;
};
Comment on lines +3 to +5
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Add a reusable container at 1152px width

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

to me feels like to generic component with very specific CSS 😅

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The goal for this one is to be consistent across all pages that needs to use the container width. This is the first page to need it

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🥳

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>
);
}
Comment on lines +11 to +24
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This can be used to easily embed help tooltips

23 changes: 23 additions & 0 deletions apps/dashboard/src/components/settings/setting-section.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
import { ReactNode } from 'react';
import { Card, CardContent, CardHeader } from '@/components/primitives/card';

interface SettingSectionProps {
title: string;
description?: string;
children: ReactNode;
}

export function SettingSection({ title, description, children }: SettingSectionProps) {
return (
<Card className="w-full overflow-hidden shadow-none">
<CardHeader>
{title}
{description && <p className="text-foreground-600 mt-1 text-xs">{description}</p>}
</CardHeader>

<CardContent className="rounded-b-xl border-t bg-neutral-50 bg-white p-3">
<div className="space-y-4 p-3">{children}</div>
</CardContent>
</Card>
);
}
11 changes: 7 additions & 4 deletions apps/dashboard/src/components/shared/external-link.tsx
Original file line number Diff line number Diff line change
@@ -1,21 +1,24 @@
import { RiExternalLinkLine } from 'react-icons/ri';
import { RiBookMarkedLine, RiBookmarkLine, RiExternalLinkLine } from 'react-icons/ri';
import { cn } from '@/utils/ui';

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

export function ExternalLink({ children, className, iconClassName, ...props }: ExternalLinkProps) {
export function ExternalLink({ children, className, variant = 'default', iconClassName, ...props }: ExternalLinkProps) {
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)}
{...props}
>
{variant === 'documentation' && <RiBookMarkedLine className={cn('size-4', iconClassName)} aria-hidden="true" />}
{variant === 'default' && <RiExternalLinkLine className={cn('size-4', iconClassName)} aria-hidden="true" />}

{children}
<RiExternalLinkLine className={cn('size-4', iconClassName)} aria-hidden="true" />
</a>
);
}
193 changes: 128 additions & 65 deletions apps/dashboard/src/pages/api-keys.tsx
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { useState } from 'react';
import { useState, ReactNode } 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,139 @@ 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">Copy and manage your public and private keys</p>

scopsy marked this conversation as resolved.
Show resolved Hide resolved
<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>

<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>
</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>
);
}
Loading